@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.
- package/.github/workflows/ci.yml +7 -6
- package/.github/workflows/release.yml +3 -3
- package/AGENTS.md +57 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +42 -342
- 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 +339 -0
- package/extensions/llm-wiki/lib/tools.ts +932 -0
- package/extensions/llm-wiki/lib/utils.ts +222 -0
- package/package.json +7 -2
- package/prompts/wiki-digest.md +5 -1
- package/prompts/wiki-discover.md +5 -1
- package/prompts/wiki-ingest.md +5 -1
- package/prompts/wiki-init.md +5 -1
- package/prompts/wiki-lint.md +5 -1
- package/prompts/wiki-query.md +5 -1
- package/prompts/wiki-run.md +5 -1
- 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/pages/analysis.md +35 -0
- package/test/llm-wiki.test.ts +51 -9
- package/extensions/llm-wiki-tools.ts +0 -705
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, 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 {
|
|
6
|
+
type Registry,
|
|
7
|
+
appendEvent,
|
|
8
|
+
buildBacklinks,
|
|
9
|
+
buildRegistry,
|
|
10
|
+
rebuildMetadata,
|
|
11
|
+
rebuildMetadataLight,
|
|
12
|
+
} from "./metadata.js";
|
|
13
|
+
import { captureFile, captureText, captureUrl } from "./source-packet.js";
|
|
14
|
+
import {
|
|
15
|
+
type VaultPaths,
|
|
16
|
+
ensureVaultStructure,
|
|
17
|
+
extractWikilinks,
|
|
18
|
+
findWikiPages,
|
|
19
|
+
fmtDate,
|
|
20
|
+
getVaultPaths,
|
|
21
|
+
readJson,
|
|
22
|
+
resolveVaultRoot,
|
|
23
|
+
writeJson,
|
|
24
|
+
} from "./utils.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* All LLM Wiki custom tools.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function getPaths(cwd = process.cwd()): VaultPaths {
|
|
31
|
+
const root = resolveVaultRoot(cwd);
|
|
32
|
+
return getVaultPaths(root);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireVault(paths: VaultPaths): { ok: true } | { ok: false; reason: string } {
|
|
36
|
+
if (!existsSync(join(paths.root, ".wiki", "config.json"))) {
|
|
37
|
+
return { ok: false, reason: `No wiki found at ${paths.root}. Run wiki_bootstrap first.` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── 1. wiki_bootstrap ──────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export function registerWikiBootstrap(pi: ExtensionAPI): void {
|
|
45
|
+
pi.registerTool({
|
|
46
|
+
name: "wiki_bootstrap",
|
|
47
|
+
label: "Wiki Bootstrap",
|
|
48
|
+
description:
|
|
49
|
+
"Initialize a new LLM Wiki vault with the 4-layer architecture. " +
|
|
50
|
+
"Creates config, templates, schema, and metadata scaffolding.",
|
|
51
|
+
promptSnippet: "Initialize a new LLM Wiki vault",
|
|
52
|
+
promptGuidelines: ["Use wiki_bootstrap when the user wants to start a new wiki."],
|
|
53
|
+
parameters: Type.Object({
|
|
54
|
+
topic: Type.String({ description: "Main topic of the wiki" }),
|
|
55
|
+
mode: Type.Optional(Type.String({ description: "personal or company (default: personal)" })),
|
|
56
|
+
root: Type.Optional(
|
|
57
|
+
Type.String({ description: "Root directory (default: current directory)" }),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
61
|
+
const root = params.root ? params.root : (ctx.cwd ?? process.cwd());
|
|
62
|
+
const mode = params.mode || "personal";
|
|
63
|
+
const paths = getVaultPaths(root);
|
|
64
|
+
|
|
65
|
+
ensureVaultStructure(paths);
|
|
66
|
+
|
|
67
|
+
const config = {
|
|
68
|
+
name: params.topic,
|
|
69
|
+
mode,
|
|
70
|
+
topic: params.topic,
|
|
71
|
+
created: fmtDate(),
|
|
72
|
+
version: "1.0",
|
|
73
|
+
};
|
|
74
|
+
writeJson(join(paths.dotWiki, "config.json"), config);
|
|
75
|
+
|
|
76
|
+
const schema = [
|
|
77
|
+
"# LLM Wiki Schema",
|
|
78
|
+
"",
|
|
79
|
+
"## Ownership Rules",
|
|
80
|
+
"",
|
|
81
|
+
"| Path | Owner | Rule |",
|
|
82
|
+
"|------|-------|------|",
|
|
83
|
+
"| raw/** | extension | immutable after capture |",
|
|
84
|
+
"| wiki/** | model + user | editable knowledge pages |",
|
|
85
|
+
"| meta/* | extension | auto-generated |",
|
|
86
|
+
"| .wiki/* | human + explicit request | operating rules |",
|
|
87
|
+
"",
|
|
88
|
+
"## Source Packet Format",
|
|
89
|
+
"",
|
|
90
|
+
"```",
|
|
91
|
+
"raw/sources/SRC-YYYY-MM-DD-NNN/",
|
|
92
|
+
" manifest.json",
|
|
93
|
+
" original/",
|
|
94
|
+
" extracted.md",
|
|
95
|
+
" attachments/",
|
|
96
|
+
"```",
|
|
97
|
+
"",
|
|
98
|
+
"## Page Types",
|
|
99
|
+
"",
|
|
100
|
+
"- **source** — what this specific source says",
|
|
101
|
+
"- **entity** — people, orgs, tools, products",
|
|
102
|
+
"- **concept** — ideas, patterns, frameworks",
|
|
103
|
+
"- **synthesis** — cross-source theses and tensions",
|
|
104
|
+
"- **analysis** — durable filed answers from queries",
|
|
105
|
+
"",
|
|
106
|
+
"## Linking Style",
|
|
107
|
+
"",
|
|
108
|
+
"- Internal: [[folder/page-name]]",
|
|
109
|
+
"- Citation: [[sources/SRC-YYYY-MM-DD-NNN]]",
|
|
110
|
+
"",
|
|
111
|
+
].join("\n");
|
|
112
|
+
writeFileSync(join(paths.root, "WIKI_SCHEMA.md"), schema, "utf-8");
|
|
113
|
+
|
|
114
|
+
rebuildMetadata(paths);
|
|
115
|
+
appendEvent(paths, { kind: "bootstrap", topic: params.topic, mode });
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: [
|
|
122
|
+
`✅ Wiki bootstrapped at \`${paths.root}\``,
|
|
123
|
+
"",
|
|
124
|
+
"**Structure:**",
|
|
125
|
+
"- raw/sources/ — immutable source packets",
|
|
126
|
+
"- wiki/ — editable knowledge pages",
|
|
127
|
+
"- meta/ — auto-generated metadata",
|
|
128
|
+
"- .wiki/ — config and templates",
|
|
129
|
+
"",
|
|
130
|
+
"Next: Use wiki_capture_source to add your first source.",
|
|
131
|
+
].join("\n"),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
details: { root, mode, topic: params.topic } as Record<string, unknown>,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── 2. wiki_capture_source ─────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function registerWikiCaptureSource(pi: ExtensionAPI): void {
|
|
143
|
+
pi.registerTool({
|
|
144
|
+
name: "wiki_capture_source",
|
|
145
|
+
label: "Wiki Capture Source",
|
|
146
|
+
description:
|
|
147
|
+
"Capture a URL, local file, or pasted text into an immutable source packet and skeleton source page.",
|
|
148
|
+
promptSnippet: "Capture a source into the wiki as an immutable packet",
|
|
149
|
+
promptGuidelines: [
|
|
150
|
+
"Use wiki_capture_source when the user provides a URL, file, or text to capture.",
|
|
151
|
+
"After capture, read the extracted text and update the skeleton source page.",
|
|
152
|
+
],
|
|
153
|
+
parameters: Type.Object({
|
|
154
|
+
url: Type.Optional(Type.String({ description: "URL to capture" })),
|
|
155
|
+
file_path: Type.Optional(Type.String({ description: "Local file path to capture" })),
|
|
156
|
+
text: Type.Optional(Type.String({ description: "Pasted text content" })),
|
|
157
|
+
title: Type.Optional(Type.String({ description: "Title for pasted text" })),
|
|
158
|
+
}),
|
|
159
|
+
async execute(_toolCallId, params, signal) {
|
|
160
|
+
const paths = getPaths();
|
|
161
|
+
const vaultCheck = requireVault(paths);
|
|
162
|
+
if (!vaultCheck.ok) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
165
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let result: {
|
|
171
|
+
sourceId: string;
|
|
172
|
+
packetPath: string;
|
|
173
|
+
sourcePagePath: string;
|
|
174
|
+
extracted: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (params.url) {
|
|
178
|
+
result = await captureUrl(pi, paths, params.url, signal);
|
|
179
|
+
} else if (params.file_path) {
|
|
180
|
+
result = await captureFile(pi, paths, params.file_path, signal);
|
|
181
|
+
} else if (params.text) {
|
|
182
|
+
result = captureText(paths, params.text, params.title);
|
|
183
|
+
} else {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: "❌ Provide one of: url, file_path, or text" }],
|
|
186
|
+
details: { error: "missing_source" } as Record<string, unknown>,
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
rebuildMetadataLight(paths);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: [
|
|
198
|
+
`✅ Captured source **${result.sourceId}**`,
|
|
199
|
+
"",
|
|
200
|
+
`- Packet: \`${result.packetPath}\``,
|
|
201
|
+
`- Skeleton page: \`${result.sourcePagePath}\``,
|
|
202
|
+
"",
|
|
203
|
+
"**Next:** Read the extracted text and update the source page with a proper summary, entities, and concepts.",
|
|
204
|
+
].join("\n"),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
details: {
|
|
208
|
+
sourceId: result.sourceId,
|
|
209
|
+
packetPath: result.packetPath,
|
|
210
|
+
sourcePagePath: result.sourcePagePath,
|
|
211
|
+
extractedPreview: result.extracted.slice(0, 300),
|
|
212
|
+
} as Record<string, unknown>,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── 3. wiki_ingest ─────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export function registerWikiIngest(pi: ExtensionAPI): void {
|
|
221
|
+
pi.registerTool({
|
|
222
|
+
name: "wiki_ingest",
|
|
223
|
+
label: "Wiki Ingest",
|
|
224
|
+
description:
|
|
225
|
+
"Process uningested source packets. Returns a batch of source IDs with extracted content for the LLM to synthesize.",
|
|
226
|
+
promptSnippet: "Ingest source packets: get batch of sources needing synthesis",
|
|
227
|
+
promptGuidelines: [
|
|
228
|
+
"Use wiki_ingest when the user wants to process captured sources.",
|
|
229
|
+
"After calling this tool, read each source's extracted.md, update its source page, create entity/concept pages, and cross-reference.",
|
|
230
|
+
"The extension auto-updates metadata — you do NOT need to edit meta/ files.",
|
|
231
|
+
],
|
|
232
|
+
parameters: Type.Object({
|
|
233
|
+
source_id: Type.Optional(
|
|
234
|
+
Type.String({ description: "Specific source ID to ingest. Leave empty for all new." }),
|
|
235
|
+
),
|
|
236
|
+
batch_size: Type.Optional(
|
|
237
|
+
Type.Number({ description: "Max sources to return (default: 3, max: 5)", default: 3 }),
|
|
238
|
+
),
|
|
239
|
+
}),
|
|
240
|
+
async execute(_toolCallId, params) {
|
|
241
|
+
const paths = getPaths();
|
|
242
|
+
const vaultCheck = requireVault(paths);
|
|
243
|
+
if (!vaultCheck.ok) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
246
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
247
|
+
isError: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const batchSize = Math.min(params.batch_size ?? 3, 5);
|
|
252
|
+
|
|
253
|
+
if (!existsSync(paths.rawSources)) {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: "No raw/sources/ directory. Capture sources first with wiki_capture_source.",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
details: { error: "no_sources" } as Record<string, unknown>,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const packets = readdirSync(paths.rawSources)
|
|
266
|
+
.filter((d) => d.startsWith("SRC-"))
|
|
267
|
+
.sort();
|
|
268
|
+
|
|
269
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
270
|
+
version: "1.0",
|
|
271
|
+
last_updated: "",
|
|
272
|
+
pages: {},
|
|
273
|
+
});
|
|
274
|
+
const ingested = new Set<string>();
|
|
275
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
276
|
+
if (entry.type === "source" && (entry as Record<string, unknown>).status !== "skeleton") {
|
|
277
|
+
const base = id.split("/").pop();
|
|
278
|
+
if (base) ingested.add(base);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let toProcess = packets.filter((p) => !ingested.has(p));
|
|
283
|
+
|
|
284
|
+
if (params.source_id) {
|
|
285
|
+
if (!toProcess.includes(params.source_id) && !packets.includes(params.source_id)) {
|
|
286
|
+
return {
|
|
287
|
+
content: [
|
|
288
|
+
{ type: "text", text: `Source ${params.source_id} not found or already ingested.` },
|
|
289
|
+
],
|
|
290
|
+
details: { source_id: params.source_id, status: "not_found" } as Record<
|
|
291
|
+
string,
|
|
292
|
+
unknown
|
|
293
|
+
>,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
toProcess = [params.source_id];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const batch = toProcess.slice(0, batchSize);
|
|
300
|
+
|
|
301
|
+
if (batch.length === 0) {
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: "✅ All sources ingested. Use wiki_capture_source to add new ones.",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
details: { ingested: ingested.size, total: packets.length } as Record<string, unknown>,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const sources = batch.map((id) => {
|
|
314
|
+
const extractedPath = join(paths.rawSources, id, "extracted.md");
|
|
315
|
+
const manifestPath = join(paths.rawSources, id, "manifest.json");
|
|
316
|
+
const extracted = existsSync(extractedPath) ? readFileSync(extractedPath, "utf-8") : "";
|
|
317
|
+
const manifest = readJson<Record<string, unknown>>(manifestPath, {});
|
|
318
|
+
return { id, extracted, manifest };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
content: [
|
|
323
|
+
{
|
|
324
|
+
type: "text",
|
|
325
|
+
text: [
|
|
326
|
+
`📥 **${batch.length} source(s) ready** (${toProcess.length - batch.length} remaining)`,
|
|
327
|
+
"",
|
|
328
|
+
...sources.map((s) =>
|
|
329
|
+
[
|
|
330
|
+
`- **${s.id}**: ${s.manifest.title || s.id}`,
|
|
331
|
+
` - Extracted: ${s.extracted.length} chars`,
|
|
332
|
+
` - Read: \`raw/sources/${s.id}/extracted.md\``,
|
|
333
|
+
].join("\n"),
|
|
334
|
+
),
|
|
335
|
+
"",
|
|
336
|
+
"**Next steps for each source:**",
|
|
337
|
+
"1. Read extracted.md",
|
|
338
|
+
"2. Update the skeleton source page in wiki/sources/",
|
|
339
|
+
"3. Create/update entity pages in wiki/entities/",
|
|
340
|
+
"4. Create/update concept pages in wiki/concepts/",
|
|
341
|
+
"5. Add [[wikilinks]] cross-references",
|
|
342
|
+
"6. Flag contradictions",
|
|
343
|
+
"",
|
|
344
|
+
"The extension will auto-update metadata when you're done.",
|
|
345
|
+
].join("\n"),
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
details: {
|
|
349
|
+
batch: sources.map((s) => s.id),
|
|
350
|
+
remaining: toProcess.length - batch.length,
|
|
351
|
+
} as Record<string, unknown>,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── 4. wiki_ensure_page ────────────────────────────────
|
|
358
|
+
|
|
359
|
+
export function registerWikiEnsurePage(pi: ExtensionAPI): void {
|
|
360
|
+
pi.registerTool({
|
|
361
|
+
name: "wiki_ensure_page",
|
|
362
|
+
label: "Wiki Ensure Page",
|
|
363
|
+
description: "Resolve or safely create a canonical wiki page. Returns the page path.",
|
|
364
|
+
promptSnippet: "Create a canonical wiki page if it doesn't exist",
|
|
365
|
+
promptGuidelines: [
|
|
366
|
+
"Use wiki_ensure_page before creating pages to avoid duplicates.",
|
|
367
|
+
"Search existing pages first with wiki_search.",
|
|
368
|
+
],
|
|
369
|
+
parameters: Type.Object({
|
|
370
|
+
type: Type.String({ description: "Page type: entity | concept | synthesis | analysis" }),
|
|
371
|
+
title: Type.String({ description: "Page title" }),
|
|
372
|
+
content: Type.Optional(
|
|
373
|
+
Type.String({ description: "Optional initial content (otherwise uses template)" }),
|
|
374
|
+
),
|
|
375
|
+
}),
|
|
376
|
+
async execute(_toolCallId, params) {
|
|
377
|
+
const paths = getPaths();
|
|
378
|
+
const vaultCheck = requireVault(paths);
|
|
379
|
+
if (!vaultCheck.ok) {
|
|
380
|
+
return {
|
|
381
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
382
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
383
|
+
isError: true,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const type = params.type as "entity" | "concept" | "synthesis" | "analysis";
|
|
388
|
+
const slug = params.title
|
|
389
|
+
.toLowerCase()
|
|
390
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
391
|
+
.trim()
|
|
392
|
+
.replace(/\s+/g, "-")
|
|
393
|
+
.slice(0, 80);
|
|
394
|
+
|
|
395
|
+
const folderMap: Record<string, string> = {
|
|
396
|
+
entity: "entities",
|
|
397
|
+
concept: "concepts",
|
|
398
|
+
synthesis: "syntheses",
|
|
399
|
+
analysis: "analyses",
|
|
400
|
+
};
|
|
401
|
+
const folder = folderMap[type] || "concepts";
|
|
402
|
+
const pagePath = join(paths.wiki, folder, `${slug}.md`);
|
|
403
|
+
|
|
404
|
+
if (existsSync(pagePath)) {
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: `✅ Page already exists: \`${pagePath}\`` }],
|
|
407
|
+
details: { path: pagePath, created: false } as Record<string, unknown>,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const today = fmtDate();
|
|
412
|
+
const template = buildPageTemplate(type, params.title, today, params.content);
|
|
413
|
+
mkdirSync(join(paths.wiki, folder), { recursive: true });
|
|
414
|
+
writeFileSync(pagePath, template, "utf-8");
|
|
415
|
+
|
|
416
|
+
appendEvent(paths, {
|
|
417
|
+
kind: "ensure_page",
|
|
418
|
+
page_type: type,
|
|
419
|
+
title: params.title,
|
|
420
|
+
path: `${folder}/${slug}`,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`` }],
|
|
425
|
+
details: { path: pagePath, created: true } as Record<string, unknown>,
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildPageTemplate(
|
|
432
|
+
type: string,
|
|
433
|
+
title: string,
|
|
434
|
+
date: string,
|
|
435
|
+
customContent?: string,
|
|
436
|
+
): string {
|
|
437
|
+
if (customContent) return customContent;
|
|
438
|
+
|
|
439
|
+
const base = `---\ntype: ${type}\ncreated: ${date}\nupdated: ${date}\nsources: []\n---\n\n# ${title}\n\n[Description to be filled]\n\n## Links\n\n- [[related-page]]\n`;
|
|
440
|
+
|
|
441
|
+
if (type === "entity") {
|
|
442
|
+
return base
|
|
443
|
+
.replace("[Description to be filled]", "One-line description.\n\n## Overview\n\n[Key facts]")
|
|
444
|
+
.replace("type: entity", "type: entity\ncategory: organization");
|
|
445
|
+
}
|
|
446
|
+
if (type === "concept") {
|
|
447
|
+
return base
|
|
448
|
+
.replace(
|
|
449
|
+
"[Description to be filled]",
|
|
450
|
+
"One-line definition.\n\n## Definition\n\n[Clear explanation]",
|
|
451
|
+
)
|
|
452
|
+
.replace("type: concept", "type: concept\ndomain: ai");
|
|
453
|
+
}
|
|
454
|
+
if (type === "synthesis") {
|
|
455
|
+
return base
|
|
456
|
+
.replace(
|
|
457
|
+
"[Description to be filled]",
|
|
458
|
+
"Cross-cutting analysis.\n\n## Question\n\n[What drove this?]",
|
|
459
|
+
)
|
|
460
|
+
.replace("sources: []", "sources_count: 0");
|
|
461
|
+
}
|
|
462
|
+
if (type === "analysis") {
|
|
463
|
+
return base.replace(
|
|
464
|
+
"[Description to be filled]",
|
|
465
|
+
"Durable answer from a query.\n\n## Question\n\n[Original question]",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return base;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── 5. wiki_search ─────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
export function registerWikiSearch(pi: ExtensionAPI): void {
|
|
474
|
+
pi.registerTool({
|
|
475
|
+
name: "wiki_search",
|
|
476
|
+
label: "Wiki Search",
|
|
477
|
+
description: "Search the wiki registry for pages matching a query.",
|
|
478
|
+
promptSnippet: "Search the wiki registry for pages",
|
|
479
|
+
promptGuidelines: ["Use wiki_search to find existing pages before creating duplicates."],
|
|
480
|
+
parameters: Type.Object({
|
|
481
|
+
query: Type.String({ description: "Search term" }),
|
|
482
|
+
type: Type.Optional(Type.String({ description: "Filter by page type" })),
|
|
483
|
+
}),
|
|
484
|
+
async execute(_toolCallId, params) {
|
|
485
|
+
const paths = getPaths();
|
|
486
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
487
|
+
version: "1.0",
|
|
488
|
+
last_updated: "",
|
|
489
|
+
pages: {},
|
|
490
|
+
});
|
|
491
|
+
const q = params.query.toLowerCase();
|
|
492
|
+
|
|
493
|
+
const matches = Object.entries(registry.pages)
|
|
494
|
+
.filter(([id, entry]) => {
|
|
495
|
+
const matchesQuery =
|
|
496
|
+
id.toLowerCase().includes(q) ||
|
|
497
|
+
String(entry.title).toLowerCase().includes(q) ||
|
|
498
|
+
String(entry.type).toLowerCase().includes(q);
|
|
499
|
+
const matchesType =
|
|
500
|
+
!params.type || String(entry.type).toLowerCase() === params.type.toLowerCase();
|
|
501
|
+
return matchesQuery && matchesType;
|
|
502
|
+
})
|
|
503
|
+
.map(([id, entry]) => ({ id, title: entry.title, type: entry.type }));
|
|
504
|
+
|
|
505
|
+
if (matches.length === 0) {
|
|
506
|
+
return {
|
|
507
|
+
content: [{ type: "text", text: `No pages found for "${params.query}"` }],
|
|
508
|
+
details: { query: params.query, matches: [] } as Record<string, unknown>,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
content: [
|
|
514
|
+
{
|
|
515
|
+
type: "text",
|
|
516
|
+
text: [
|
|
517
|
+
`🔍 **${matches.length} result(s)** for "${params.query}":`,
|
|
518
|
+
"",
|
|
519
|
+
...matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
|
|
520
|
+
].join("\n"),
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
details: { query: params.query, matches } as Record<string, unknown>,
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ─── 6. wiki_lint ───────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
export function registerWikiLint(pi: ExtensionAPI): void {
|
|
532
|
+
pi.registerTool({
|
|
533
|
+
name: "wiki_lint",
|
|
534
|
+
label: "Wiki Lint",
|
|
535
|
+
description:
|
|
536
|
+
"Health check the wiki. Scans for orphans, missing pages, contradictions, gaps. Optionally auto-fixes.",
|
|
537
|
+
promptSnippet: "Lint the wiki for health issues",
|
|
538
|
+
promptGuidelines: [
|
|
539
|
+
"Use wiki_lint when the user asks to check wiki health.",
|
|
540
|
+
"Contradictions always need human review.",
|
|
541
|
+
],
|
|
542
|
+
parameters: Type.Object({
|
|
543
|
+
auto_fix: Type.Optional(
|
|
544
|
+
Type.Boolean({ description: "Auto-fix orphans and missing pages", default: false }),
|
|
545
|
+
),
|
|
546
|
+
}),
|
|
547
|
+
async execute(_toolCallId, params) {
|
|
548
|
+
const paths = getPaths();
|
|
549
|
+
const vaultCheck = requireVault(paths);
|
|
550
|
+
if (!vaultCheck.ok) {
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
553
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
554
|
+
isError: true,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const pages = findWikiPages(paths.wiki);
|
|
559
|
+
const registry = buildRegistry(paths);
|
|
560
|
+
buildBacklinks(paths, registry); // ensures backlinks.json is current
|
|
561
|
+
|
|
562
|
+
const findings: string[] = [];
|
|
563
|
+
let orphans = 0;
|
|
564
|
+
let missingPages = 0;
|
|
565
|
+
let contradictions = 0;
|
|
566
|
+
const gaps: Array<{ topic: string; mentionedBy: string[] }> = [];
|
|
567
|
+
|
|
568
|
+
const allPageIds = new Set(pages.map((p) => p.relative));
|
|
569
|
+
const linkCounts: Record<string, number> = {};
|
|
570
|
+
|
|
571
|
+
for (const page of pages) {
|
|
572
|
+
const links = extractWikilinks(page.content);
|
|
573
|
+
for (const link of links) {
|
|
574
|
+
if (!allPageIds.has(link)) {
|
|
575
|
+
missingPages++;
|
|
576
|
+
findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
|
|
577
|
+
const existing = gaps.find((g) => g.topic === link);
|
|
578
|
+
if (existing) {
|
|
579
|
+
if (!existing.mentionedBy.includes(page.relative))
|
|
580
|
+
existing.mentionedBy.push(page.relative);
|
|
581
|
+
} else {
|
|
582
|
+
gaps.push({ topic: link, mentionedBy: [page.relative] });
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
linkCounts[link] = (linkCounts[link] || 0) + 1;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (const page of pages) {
|
|
591
|
+
if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
|
|
592
|
+
orphans++;
|
|
593
|
+
findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
for (const page of pages) {
|
|
598
|
+
if (page.content.includes("⚠️ **Contradiction")) {
|
|
599
|
+
contradictions++;
|
|
600
|
+
findings.push(`Contradiction flagged in [[${page.relative}]]`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let fixesApplied = 0;
|
|
605
|
+
if (params.auto_fix) {
|
|
606
|
+
for (const gap of gaps) {
|
|
607
|
+
if (gap.mentionedBy.length >= 2) {
|
|
608
|
+
const folder = gap.topic.includes("/") ? gap.topic.split("/")[0] : "concepts";
|
|
609
|
+
const name = gap.topic.includes("/") ? gap.topic.split("/").pop()! : gap.topic;
|
|
610
|
+
const pagePath = join(paths.wiki, folder, `${name}.md`);
|
|
611
|
+
if (!existsSync(pagePath)) {
|
|
612
|
+
mkdirSync(join(paths.wiki, folder), { recursive: true });
|
|
613
|
+
writeFileSync(
|
|
614
|
+
pagePath,
|
|
615
|
+
`---\ntype: concept\ncreated: ${fmtDate()}\nupdated: ${fmtDate()}\nsources: []\nstatus: stub\n---\n\n# ${name.replace(/-/g, " ")}\n\n_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy.map((r) => `[[${r}]]`).join(", ")}_\n`,
|
|
616
|
+
"utf-8",
|
|
617
|
+
);
|
|
618
|
+
fixesApplied++;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
writeJson(join(paths.discoveries, "gaps.json"), {
|
|
625
|
+
gaps,
|
|
626
|
+
generated: new Date().toISOString(),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const reportLines = [
|
|
630
|
+
"# Wiki Lint Report",
|
|
631
|
+
`Generated: ${fmtDate()}`,
|
|
632
|
+
"",
|
|
633
|
+
"## Summary",
|
|
634
|
+
`- Total pages: ${pages.length}`,
|
|
635
|
+
`- Orphans: ${orphans}`,
|
|
636
|
+
`- Missing pages: ${missingPages}`,
|
|
637
|
+
`- Contradictions: ${contradictions}`,
|
|
638
|
+
params.auto_fix ? `- Fixes applied: ${fixesApplied}` : "",
|
|
639
|
+
"",
|
|
640
|
+
"## Findings",
|
|
641
|
+
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
|
|
642
|
+
"",
|
|
643
|
+
].filter(Boolean);
|
|
644
|
+
|
|
645
|
+
const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
|
|
646
|
+
mkdirSync(paths.outputs, { recursive: true });
|
|
647
|
+
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
|
|
648
|
+
|
|
649
|
+
appendEvent(paths, {
|
|
650
|
+
kind: "lint",
|
|
651
|
+
orphans,
|
|
652
|
+
missing_pages: missingPages,
|
|
653
|
+
contradictions,
|
|
654
|
+
auto_fix: params.auto_fix ?? false,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
rebuildMetadataLight(paths);
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
content: [
|
|
661
|
+
{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: [
|
|
664
|
+
"🧹 **Lint complete**",
|
|
665
|
+
"",
|
|
666
|
+
`- Pages: ${pages.length}`,
|
|
667
|
+
`- Orphans: ${orphans}`,
|
|
668
|
+
`- Missing: ${missingPages}`,
|
|
669
|
+
`- Contradictions: ${contradictions}`,
|
|
670
|
+
params.auto_fix ? `- Auto-fixes: ${fixesApplied}` : "",
|
|
671
|
+
"",
|
|
672
|
+
`📄 Report: \`${reportPath}\``,
|
|
673
|
+
gaps.length > 0 ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
|
|
674
|
+
]
|
|
675
|
+
.filter(Boolean)
|
|
676
|
+
.join("\n"),
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
details: {
|
|
680
|
+
pages: pages.length,
|
|
681
|
+
orphans,
|
|
682
|
+
missingPages,
|
|
683
|
+
contradictions,
|
|
684
|
+
reportPath,
|
|
685
|
+
gaps: gaps.length,
|
|
686
|
+
} as Record<string, unknown>,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ─── 7. wiki_status ─────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
695
|
+
pi.registerTool({
|
|
696
|
+
name: "wiki_status",
|
|
697
|
+
label: "Wiki Status",
|
|
698
|
+
description: "Report wiki health and stats instantly from generated registry.",
|
|
699
|
+
promptSnippet: "Report wiki health and stats",
|
|
700
|
+
promptGuidelines: ["Use wiki_status for a quick overview."],
|
|
701
|
+
parameters: Type.Object({}),
|
|
702
|
+
async execute() {
|
|
703
|
+
const paths = getPaths();
|
|
704
|
+
const vaultCheck = requireVault(paths);
|
|
705
|
+
if (!vaultCheck.ok) {
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
708
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
709
|
+
isError: true,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
714
|
+
version: "1.0",
|
|
715
|
+
last_updated: "",
|
|
716
|
+
pages: {},
|
|
717
|
+
});
|
|
718
|
+
const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
|
|
719
|
+
const config = readJson<Record<string, unknown>>(join(paths.dotWiki, "config.json"), {});
|
|
720
|
+
|
|
721
|
+
const byType: Record<string, number> = {};
|
|
722
|
+
for (const entry of Object.values(registry.pages)) {
|
|
723
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const orphanCount = Object.entries(backlinks).filter(
|
|
727
|
+
([, inbound]) => inbound.length === 0,
|
|
728
|
+
).length;
|
|
729
|
+
const gaps = readJson<{ gaps?: unknown[] }>(join(paths.discoveries, "gaps.json"), {
|
|
730
|
+
gaps: [],
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const health =
|
|
734
|
+
Object.keys(registry.pages).length === 0
|
|
735
|
+
? "🔴 Empty"
|
|
736
|
+
: orphanCount > 5
|
|
737
|
+
? "⚠️ Warning"
|
|
738
|
+
: "✅ Good";
|
|
739
|
+
|
|
740
|
+
const lines = [
|
|
741
|
+
"📊 LLM Wiki Status",
|
|
742
|
+
"══════════════════",
|
|
743
|
+
`Topic: ${config.topic || "Unknown"}`,
|
|
744
|
+
`Mode: ${config.mode || "personal"}`,
|
|
745
|
+
`Pages: ${Object.keys(registry.pages).length}`,
|
|
746
|
+
...Object.entries(byType).map(([t, c]) => ` - ${t}s: ${c}`),
|
|
747
|
+
`Orphans: ${orphanCount}`,
|
|
748
|
+
`Gaps: ${gaps.gaps?.length || 0}`,
|
|
749
|
+
`Health: ${health}`,
|
|
750
|
+
`Last updated: ${registry.last_updated || "Never"}`,
|
|
751
|
+
];
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
755
|
+
details: {
|
|
756
|
+
topic: config.topic,
|
|
757
|
+
mode: config.mode,
|
|
758
|
+
totalPages: Object.keys(registry.pages).length,
|
|
759
|
+
byType,
|
|
760
|
+
orphans: orphanCount,
|
|
761
|
+
gaps: gaps.gaps?.length || 0,
|
|
762
|
+
health,
|
|
763
|
+
} as Record<string, unknown>,
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ─── 8. wiki_rebuild_meta ───────────────────────────────
|
|
770
|
+
|
|
771
|
+
export function registerWikiRebuildMeta(pi: ExtensionAPI): void {
|
|
772
|
+
pi.registerTool({
|
|
773
|
+
name: "wiki_rebuild_meta",
|
|
774
|
+
label: "Wiki Rebuild Meta",
|
|
775
|
+
description: "Force a full metadata rebuild (registry, backlinks, index, log).",
|
|
776
|
+
promptSnippet: "Rebuild all wiki metadata",
|
|
777
|
+
promptGuidelines: ["Use wiki_rebuild_meta if metadata seems out of sync."],
|
|
778
|
+
parameters: Type.Object({}),
|
|
779
|
+
async execute() {
|
|
780
|
+
const paths = getPaths();
|
|
781
|
+
const vaultCheck = requireVault(paths);
|
|
782
|
+
if (!vaultCheck.ok) {
|
|
783
|
+
return {
|
|
784
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
785
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
786
|
+
isError: true,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
rebuildMetadata(paths);
|
|
791
|
+
appendEvent(paths, { kind: "rebuild_meta" });
|
|
792
|
+
|
|
793
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
794
|
+
version: "1.0",
|
|
795
|
+
last_updated: "",
|
|
796
|
+
pages: {},
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
802
|
+
type: "text",
|
|
803
|
+
text: `✅ Metadata rebuilt. ${Object.keys(registry.pages).length} pages indexed.`,
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
details: { pageCount: Object.keys(registry.pages).length } as Record<string, unknown>,
|
|
807
|
+
};
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ─── 9. wiki_log_event ──────────────────────────────────
|
|
813
|
+
|
|
814
|
+
export function registerWikiLogEvent(pi: ExtensionAPI): void {
|
|
815
|
+
pi.registerTool({
|
|
816
|
+
name: "wiki_log_event",
|
|
817
|
+
label: "Wiki Log Event",
|
|
818
|
+
description: "Append a structured event to meta/events.jsonl and regenerate meta/log.md.",
|
|
819
|
+
promptSnippet: "Log an event to the wiki activity log",
|
|
820
|
+
promptGuidelines: ["Use wiki_log_event to record significant actions manually."],
|
|
821
|
+
parameters: Type.Object({
|
|
822
|
+
kind: Type.String({ description: "Event kind (e.g., ingest, query, decision)" }),
|
|
823
|
+
details: Type.Optional(Type.Object({}, { description: "Additional event fields" })),
|
|
824
|
+
}),
|
|
825
|
+
async execute(_toolCallId, params) {
|
|
826
|
+
const paths = getPaths();
|
|
827
|
+
const vaultCheck = requireVault(paths);
|
|
828
|
+
if (!vaultCheck.ok) {
|
|
829
|
+
return {
|
|
830
|
+
content: [{ type: "text", text: vaultCheck.reason }],
|
|
831
|
+
details: { error: vaultCheck.reason } as Record<string, unknown>,
|
|
832
|
+
isError: true,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
appendEvent(paths, { kind: params.kind, ...params.details });
|
|
837
|
+
|
|
838
|
+
// Regenerate log.md
|
|
839
|
+
const { buildLogMarkdown } = await import("./metadata.js");
|
|
840
|
+
const log = buildLogMarkdown(paths);
|
|
841
|
+
writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
content: [{ type: "text", text: `✅ Event logged: ${params.kind}` }],
|
|
845
|
+
details: { kind: params.kind } as Record<string, unknown>,
|
|
846
|
+
};
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ─── 10. wiki_watch ─────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
854
|
+
pi.registerTool({
|
|
855
|
+
name: "wiki_watch",
|
|
856
|
+
label: "Wiki Watch",
|
|
857
|
+
description: "Schedule automatic wiki updates (discover → ingest → lint) via pi's cron system.",
|
|
858
|
+
promptSnippet: "Schedule auto-updates for the wiki",
|
|
859
|
+
promptGuidelines: [
|
|
860
|
+
"Use wiki_watch when the user wants the wiki to stay current automatically.",
|
|
861
|
+
],
|
|
862
|
+
parameters: Type.Object({
|
|
863
|
+
interval: Type.String({ description: "daily, weekly, hourly, or stop" }),
|
|
864
|
+
}),
|
|
865
|
+
async execute(_toolCallId, params) {
|
|
866
|
+
if (params.interval === "stop") {
|
|
867
|
+
return {
|
|
868
|
+
content: [
|
|
869
|
+
{
|
|
870
|
+
type: "text",
|
|
871
|
+
text: [
|
|
872
|
+
"🛑 To stop wiki auto-updates:",
|
|
873
|
+
"",
|
|
874
|
+
"```",
|
|
875
|
+
"schedule_prompt action=list",
|
|
876
|
+
"```",
|
|
877
|
+
"Find the wiki job IDs, then:",
|
|
878
|
+
"",
|
|
879
|
+
"```",
|
|
880
|
+
"schedule_prompt action=remove jobId=<id>",
|
|
881
|
+
"```",
|
|
882
|
+
].join("\n"),
|
|
883
|
+
},
|
|
884
|
+
],
|
|
885
|
+
details: { action: "stop_instructions" } as Record<string, unknown>,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const intervals: Record<string, { cron: string; label: string }> = {
|
|
890
|
+
daily: { cron: "0 0 8 * * *", label: "Daily at 8:00 AM" },
|
|
891
|
+
weekly: { cron: "0 0 9 * * 1", label: "Weekly on Monday at 9:00 AM" },
|
|
892
|
+
hourly: { cron: "0 0 * * * *", label: "Every hour" },
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const config = intervals[params.interval];
|
|
896
|
+
if (!config) {
|
|
897
|
+
return {
|
|
898
|
+
content: [
|
|
899
|
+
{
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `❌ Unknown interval: "${params.interval}". Use: daily, weekly, hourly, or stop.`,
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
details: { error: "bad_interval" } as Record<string, unknown>,
|
|
905
|
+
isError: true,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
content: [
|
|
911
|
+
{
|
|
912
|
+
type: "text",
|
|
913
|
+
text: [
|
|
914
|
+
`⏰ To set up ${config.label} wiki updates, run:`,
|
|
915
|
+
"",
|
|
916
|
+
"```",
|
|
917
|
+
`schedule_prompt action=add schedule="${config.cron}" prompt="Run /wiki:run for the LLM Wiki" name="llm-wiki-autoupdate"`,
|
|
918
|
+
"```",
|
|
919
|
+
"",
|
|
920
|
+
"This will auto-discover, ingest, and lint on schedule.",
|
|
921
|
+
].join("\n"),
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
details: {
|
|
925
|
+
interval: params.interval,
|
|
926
|
+
cronSchedule: config.cron,
|
|
927
|
+
label: config.label,
|
|
928
|
+
} as Record<string, unknown>,
|
|
929
|
+
};
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
}
|