@zosmaai/pi-llm-wiki 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.de.md +19 -1
- package/README.es.md +19 -1
- package/README.fr.md +19 -1
- package/README.hi.md +19 -1
- package/README.ja.md +19 -1
- package/README.ko.md +19 -1
- package/README.md +38 -3
- package/README.pt.md +19 -1
- package/README.ru.md +19 -1
- package/README.zh.md +19 -1
- package/dist/extensions/llm-wiki/lib/bootstrap.js +4 -1
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +129 -29
- package/dist/extensions/llm-wiki/lib/metadata.js +37 -31
- package/dist/extensions/llm-wiki/lib/model-command.js +0 -1
- package/dist/extensions/llm-wiki/lib/runtime.js +0 -4
- package/dist/extensions/llm-wiki/lib/source-packet.js +1 -1
- package/dist/extensions/llm-wiki/lib/task-config.js +29 -0
- package/dist/extensions/llm-wiki/lib/tools.js +7 -0
- package/dist/extensions/llm-wiki/lib/utils.js +6 -0
- package/dist/mcp/index.js +2 -1
- package/docs/api.md +5 -2
- package/docs/architecture.md +5 -2
- package/docs/configuration.md +25 -0
- package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
- package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +17 -2
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +6 -2
- package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
- package/extensions/llm-wiki/lib/bootstrap.ts +4 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +161 -26
- package/extensions/llm-wiki/lib/knowledge-document.ts +2 -0
- package/extensions/llm-wiki/lib/metadata.ts +38 -31
- package/extensions/llm-wiki/lib/model-command.ts +0 -1
- package/extensions/llm-wiki/lib/runtime.ts +0 -3
- package/extensions/llm-wiki/lib/source-packet.ts +1 -1
- package/extensions/llm-wiki/lib/task-config.ts +36 -0
- package/extensions/llm-wiki/lib/tools.ts +9 -0
- package/extensions/llm-wiki/lib/utils.ts +7 -0
- package/mcp/index.ts +2 -1
- package/package.json +2 -2
- package/skills/llm-wiki/SKILL.md +4 -2
|
@@ -48,13 +48,107 @@ export const CommitSynthesisSchema = Type.Object({
|
|
|
48
48
|
})),
|
|
49
49
|
});
|
|
50
50
|
// ── deterministic persistence (no LLM) ────────────────────
|
|
51
|
-
|
|
51
|
+
/** Localized headings for generated pages (issue #124). */
|
|
52
|
+
const HEADINGS = {
|
|
53
|
+
ru: {
|
|
54
|
+
summary: "Резюме",
|
|
55
|
+
keyTakeaways: "Ключевые выводы",
|
|
56
|
+
entitiesMentioned: "Упомянутые сущности",
|
|
57
|
+
conceptsMentioned: "Упомянутые концепции",
|
|
58
|
+
notableQuotes: "Заметные цитаты",
|
|
59
|
+
contradictions: "Противоречия",
|
|
60
|
+
sourcePacket: "Пакет источника",
|
|
61
|
+
overview: "Обзор",
|
|
62
|
+
definition: "Определение",
|
|
63
|
+
noneRecorded: "[Не записано]",
|
|
64
|
+
none: "[Нет]",
|
|
65
|
+
id: "ID",
|
|
66
|
+
extracted: "Извлечено",
|
|
67
|
+
manifest: "Манифест",
|
|
68
|
+
contradiction: "⚠️ **Противоречие**",
|
|
69
|
+
},
|
|
70
|
+
fr: {
|
|
71
|
+
summary: "Résumé",
|
|
72
|
+
keyTakeaways: "Points clés",
|
|
73
|
+
entitiesMentioned: "Entités mentionnées",
|
|
74
|
+
conceptsMentioned: "Concepts mentionnés",
|
|
75
|
+
notableQuotes: "Citations notables",
|
|
76
|
+
contradictions: "Contradictions",
|
|
77
|
+
sourcePacket: "Paquet source",
|
|
78
|
+
overview: "Aperçu",
|
|
79
|
+
definition: "Définition",
|
|
80
|
+
noneRecorded: "[Aucun enregistré]",
|
|
81
|
+
none: "[Aucun]",
|
|
82
|
+
id: "ID",
|
|
83
|
+
extracted: "Extrait",
|
|
84
|
+
manifest: "Manifeste",
|
|
85
|
+
contradiction: "⚠️ **Contradiction**",
|
|
86
|
+
},
|
|
87
|
+
de: {
|
|
88
|
+
summary: "Zusammenfassung",
|
|
89
|
+
keyTakeaways: "Wichtige Erkenntnisse",
|
|
90
|
+
entitiesMentioned: "Erwähnte Entitäten",
|
|
91
|
+
conceptsMentioned: "Erwähnte Konzepte",
|
|
92
|
+
notableQuotes: "Bemerkenswerte Zitate",
|
|
93
|
+
contradictions: "Widersprüche",
|
|
94
|
+
sourcePacket: "Quellenpaket",
|
|
95
|
+
overview: "Übersicht",
|
|
96
|
+
definition: "Definition",
|
|
97
|
+
noneRecorded: "[Keine aufgezeichnet]",
|
|
98
|
+
none: "[Keine]",
|
|
99
|
+
id: "ID",
|
|
100
|
+
extracted: "Extrahiert",
|
|
101
|
+
manifest: "Manifest",
|
|
102
|
+
contradiction: "⚠️ **Widerspruch**",
|
|
103
|
+
},
|
|
104
|
+
ja: {
|
|
105
|
+
summary: "要約",
|
|
106
|
+
keyTakeaways: "主な要点",
|
|
107
|
+
entitiesMentioned: "言及されたエンティティ",
|
|
108
|
+
conceptsMentioned: "言及された概念",
|
|
109
|
+
notableQuotes: "注目すべき引用",
|
|
110
|
+
contradictions: "矛盾",
|
|
111
|
+
sourcePacket: "ソースパケット",
|
|
112
|
+
overview: "概要",
|
|
113
|
+
definition: "定義",
|
|
114
|
+
noneRecorded: "[記録なし]",
|
|
115
|
+
none: "[なし]",
|
|
116
|
+
id: "ID",
|
|
117
|
+
extracted: "抽出済み",
|
|
118
|
+
manifest: "マニフェスト",
|
|
119
|
+
contradiction: "⚠️ **矛盾**",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
function getHeadings(lang) {
|
|
123
|
+
if (lang && HEADINGS[lang])
|
|
124
|
+
return HEADINGS[lang];
|
|
125
|
+
// English defaults
|
|
126
|
+
return {
|
|
127
|
+
summary: "Summary",
|
|
128
|
+
keyTakeaways: "Key Takeaways",
|
|
129
|
+
entitiesMentioned: "Entities Mentioned",
|
|
130
|
+
conceptsMentioned: "Concepts Mentioned",
|
|
131
|
+
notableQuotes: "Notable Quotes",
|
|
132
|
+
contradictions: "Contradictions",
|
|
133
|
+
sourcePacket: "Source Packet",
|
|
134
|
+
overview: "Overview",
|
|
135
|
+
definition: "Definition",
|
|
136
|
+
noneRecorded: "[None recorded]",
|
|
137
|
+
none: "[None]",
|
|
138
|
+
id: "ID",
|
|
139
|
+
extracted: "Extracted",
|
|
140
|
+
manifest: "Manifest",
|
|
141
|
+
contradiction: "⚠️ **Contradiction**",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildEntityPageBody(title, description, sourceId, lang) {
|
|
52
145
|
const desc = description.trim() || "One-line description.";
|
|
146
|
+
const h = getHeadings(lang);
|
|
53
147
|
return `# ${title}
|
|
54
148
|
|
|
55
149
|
${desc}
|
|
56
150
|
|
|
57
|
-
##
|
|
151
|
+
## ${h.overview}
|
|
58
152
|
|
|
59
153
|
[Key facts]
|
|
60
154
|
|
|
@@ -62,13 +156,14 @@ ${desc}
|
|
|
62
156
|
|
|
63
157
|
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
64
158
|
}
|
|
65
|
-
function buildConceptPageBody(title, definition, sourceId) {
|
|
159
|
+
function buildConceptPageBody(title, definition, sourceId, lang) {
|
|
66
160
|
const def = definition.trim() || "One-line definition.";
|
|
161
|
+
const h = getHeadings(lang);
|
|
67
162
|
return `# ${title}
|
|
68
163
|
|
|
69
164
|
${def}
|
|
70
165
|
|
|
71
|
-
##
|
|
166
|
+
## ${h.definition}
|
|
72
167
|
|
|
73
168
|
[Clear explanation]
|
|
74
169
|
|
|
@@ -77,62 +172,63 @@ ${def}
|
|
|
77
172
|
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
78
173
|
}
|
|
79
174
|
/** Rebuild the source page body from synthesis data, marking it ingested. */
|
|
80
|
-
export function buildIngestedSourcePageBody(manifest, data, _date) {
|
|
175
|
+
export function buildIngestedSourcePageBody(manifest, data, _date, lang) {
|
|
81
176
|
const id = String(manifest.id);
|
|
82
177
|
const title = String(manifest.title || id);
|
|
83
178
|
const url = manifest.url ? `\n> _Original: [${manifest.url}](${manifest.url})_` : "";
|
|
179
|
+
const h = getHeadings(lang);
|
|
84
180
|
const takeaways = data.key_takeaways.length > 0
|
|
85
181
|
? data.key_takeaways.map((t) => `- ${t.trim()}`).join("\n")
|
|
86
|
-
:
|
|
182
|
+
: `- ${h.noneRecorded}`;
|
|
87
183
|
const entities = data.entities.length > 0
|
|
88
184
|
? data.entities.map((e) => `- [${e.title}](/entities/${slugify(e.title)}.md)`).join("\n")
|
|
89
|
-
:
|
|
185
|
+
: `- ${h.none}`;
|
|
90
186
|
const concepts = data.concepts.length > 0
|
|
91
187
|
? data.concepts.map((c) => `- [${c.title}](/concepts/${slugify(c.title)}.md)`).join("\n")
|
|
92
|
-
:
|
|
188
|
+
: `- ${h.none}`;
|
|
93
189
|
const quotes = data.quotes && data.quotes.length > 0
|
|
94
190
|
? data.quotes
|
|
95
191
|
.map((q) => `> ${q.text.trim()}${q.attribution ? ` — ${q.attribution}` : ""}`)
|
|
96
192
|
.join("\n\n")
|
|
97
|
-
:
|
|
193
|
+
: `> ${h.noneRecorded}`;
|
|
98
194
|
const contradictions = data.contradictions && data.contradictions.length > 0
|
|
99
|
-
? `\n##
|
|
195
|
+
? `\n## ${h.contradictions}\n\n${data.contradictions.map((c) => `${h.contradiction}: ${c.trim()}`).join("\n")}\n`
|
|
100
196
|
: "";
|
|
101
197
|
return `# ${title}${url}
|
|
102
198
|
|
|
103
|
-
##
|
|
199
|
+
## ${h.summary}
|
|
104
200
|
|
|
105
201
|
${data.summary.trim()}
|
|
106
202
|
|
|
107
|
-
##
|
|
203
|
+
## ${h.keyTakeaways}
|
|
108
204
|
|
|
109
205
|
${takeaways}
|
|
110
206
|
|
|
111
|
-
##
|
|
207
|
+
## ${h.entitiesMentioned}
|
|
112
208
|
|
|
113
209
|
${entities}
|
|
114
210
|
|
|
115
|
-
##
|
|
211
|
+
## ${h.conceptsMentioned}
|
|
116
212
|
|
|
117
213
|
${concepts}
|
|
118
214
|
|
|
119
|
-
##
|
|
215
|
+
## ${h.notableQuotes}
|
|
120
216
|
|
|
121
217
|
${quotes}
|
|
122
|
-
${contradictions}##
|
|
218
|
+
${contradictions}## ${h.sourcePacket}
|
|
123
219
|
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
220
|
+
- **${h.id}:** \`sources/${id}\`
|
|
221
|
+
- **${h.extracted}:** \`raw/sources/${id}/extracted.md\`
|
|
222
|
+
- **${h.manifest}:** \`raw/sources/${id}/manifest.json\`
|
|
127
223
|
`;
|
|
128
224
|
}
|
|
129
225
|
/** Rebuild the source page from synthesis data, marking it ingested. */
|
|
130
|
-
export function buildIngestedSourcePage(manifest, data, date) {
|
|
226
|
+
export function buildIngestedSourcePage(manifest, data, date, lang) {
|
|
131
227
|
const id = String(manifest.id);
|
|
132
228
|
const title = String(manifest.title || id);
|
|
133
229
|
const format = String(manifest.format || "unknown");
|
|
134
230
|
const captured = String(manifest.captured || date);
|
|
135
|
-
const body = buildIngestedSourcePageBody(manifest, data, date);
|
|
231
|
+
const body = buildIngestedSourcePageBody(manifest, data, date, lang);
|
|
136
232
|
const doc = createKnowledgeDocument(`sources/${id}.md`, {
|
|
137
233
|
type: "source",
|
|
138
234
|
title,
|
|
@@ -150,7 +246,7 @@ export function buildIngestedSourcePage(manifest, data, date) {
|
|
|
150
246
|
* ingested), create missing entity/concept pages (existing pages are linked,
|
|
151
247
|
* never overwritten), and log the event. Pure file I/O — no LLM, no network.
|
|
152
248
|
*/
|
|
153
|
-
export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate()) {
|
|
249
|
+
export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(), lang) {
|
|
154
250
|
const result = {
|
|
155
251
|
sourceId,
|
|
156
252
|
sourcePage: join(paths.wiki, "sources", `${sourceId}.md`),
|
|
@@ -177,7 +273,7 @@ export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(
|
|
|
177
273
|
return { ok: false, sourceId, diagnostics: parsed.diagnostics };
|
|
178
274
|
sourceDocument = patchKnowledgeDocument(parsed.document, {
|
|
179
275
|
fields: { status: "ingested", updated: date },
|
|
180
|
-
body: buildIngestedSourcePageBody(manifest, data, date),
|
|
276
|
+
body: buildIngestedSourcePageBody(manifest, data, date, lang),
|
|
181
277
|
});
|
|
182
278
|
}
|
|
183
279
|
else {
|
|
@@ -190,7 +286,7 @@ export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(
|
|
|
190
286
|
captured: String(manifest.captured || date),
|
|
191
287
|
status: "ingested",
|
|
192
288
|
updated: date,
|
|
193
|
-
}, buildIngestedSourcePageBody(manifest, data, date));
|
|
289
|
+
}, buildIngestedSourcePageBody(manifest, data, date, lang));
|
|
194
290
|
}
|
|
195
291
|
mkdirSync(join(paths.wiki, "sources"), { recursive: true });
|
|
196
292
|
writeKnowledgeDocumentFile(result.sourcePage, sourceDocument);
|
|
@@ -211,7 +307,7 @@ export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(
|
|
|
211
307
|
description: e.description.trim() || "One-line description.",
|
|
212
308
|
created: date,
|
|
213
309
|
updated: date,
|
|
214
|
-
}, buildEntityPageBody(e.title, e.description, sourceId), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
310
|
+
}, buildEntityPageBody(e.title, e.description, sourceId, lang), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
215
311
|
writeKnowledgeDocumentFile(pagePath, entityDoc);
|
|
216
312
|
result.entitiesCreated.push(slug);
|
|
217
313
|
}
|
|
@@ -233,7 +329,7 @@ export function commitSynthesis(paths, sourceId, manifest, data, date = fmtDate(
|
|
|
233
329
|
description: c.definition.trim() || "One-line definition.",
|
|
234
330
|
created: date,
|
|
235
331
|
updated: date,
|
|
236
|
-
}, buildConceptPageBody(c.title, c.definition, sourceId), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
332
|
+
}, buildConceptPageBody(c.title, c.definition, sourceId, lang), [{ id: sourceId, resource: `/sources/${sourceId}.md` }]);
|
|
237
333
|
writeKnowledgeDocumentFile(pagePath, conceptDoc);
|
|
238
334
|
result.conceptsCreated.push(slug);
|
|
239
335
|
}
|
|
@@ -269,10 +365,14 @@ Rules:
|
|
|
269
365
|
* synthesis.
|
|
270
366
|
*/
|
|
271
367
|
export async function runIngestSynthesis(args) {
|
|
272
|
-
const { model, apiKey, headers, paths, sourceId, manifest, extracted, maxChars, signal } = args;
|
|
368
|
+
const { model, apiKey, headers, paths, sourceId, manifest, extracted, maxChars, signal, synthesisLanguage, } = args;
|
|
273
369
|
const content = extracted.slice(0, maxChars ?? 24_000);
|
|
274
370
|
if (!content.trim())
|
|
275
371
|
return undefined;
|
|
372
|
+
const languageInstruction = synthesisLanguage
|
|
373
|
+
? `\n\nWrite all generated content in ${synthesisLanguage}, including titles, headings, summaries, descriptions, and concept/entity names. Only preserve code, API names, file paths, commands, exact technical identifiers, and verbatim quotations in their original form.`
|
|
374
|
+
: "";
|
|
375
|
+
const systemPrompt = INGEST_SYSTEM + languageInstruction;
|
|
276
376
|
let committed;
|
|
277
377
|
const commitTool = {
|
|
278
378
|
name: "commit_synthesis",
|
|
@@ -280,7 +380,7 @@ export async function runIngestSynthesis(args) {
|
|
|
280
380
|
description: "Persist the structured synthesis of this source into wiki pages. Call exactly once.",
|
|
281
381
|
parameters: CommitSynthesisSchema,
|
|
282
382
|
execute: async (_id, params) => {
|
|
283
|
-
const outcome = commitSynthesis(paths, sourceId, manifest, params);
|
|
383
|
+
const outcome = commitSynthesis(paths, sourceId, manifest, params, undefined, synthesisLanguage);
|
|
284
384
|
if (!outcome.ok) {
|
|
285
385
|
return {
|
|
286
386
|
content: [{ type: "text", text: `Failed: ${outcome.diagnostics[0].message}` }],
|
|
@@ -299,7 +399,7 @@ export async function runIngestSynthesis(args) {
|
|
|
299
399
|
model,
|
|
300
400
|
apiKey,
|
|
301
401
|
headers,
|
|
302
|
-
systemPrompt
|
|
402
|
+
systemPrompt,
|
|
303
403
|
userPrompt,
|
|
304
404
|
tools: [commitTool],
|
|
305
405
|
signal,
|
|
@@ -26,17 +26,21 @@ export function rebuildMetadata(paths) {
|
|
|
26
26
|
// Step 4: Build backlinks from discovered documents
|
|
27
27
|
const knownIds = new Set(documents.map((d) => d.id));
|
|
28
28
|
const backlinks = buildBacklinks(documents, knownIds, allDiagnostics);
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const eventSource = readEventSource(join(paths.meta, "events.jsonl"));
|
|
30
|
+
const eventLogResult = eventSource.available ? buildOkfLog(eventSource.content) : undefined;
|
|
31
|
+
if (eventLogResult)
|
|
32
|
+
allDiagnostics.push(...eventLogResult.diagnostics);
|
|
33
|
+
else if (!eventSource.available)
|
|
34
|
+
allDiagnostics.push(eventSource.diagnostic);
|
|
31
35
|
// Step 5: Build meta/index.md
|
|
32
36
|
const metaIndex = buildIndexMarkdown(registry);
|
|
33
37
|
// Step 6: Build meta/log.md (existing rich format)
|
|
34
|
-
const metaLog = buildLogMarkdown(
|
|
38
|
+
const metaLog = eventSource.available ? buildLogMarkdown(eventSource.content) : undefined;
|
|
35
39
|
// Step 7: Build OKF projections if in okf-0.2 mode
|
|
36
40
|
const okfIndexes = vaultState.knowledgeFormat === "okf-0.2"
|
|
37
41
|
? buildDirectoryIndexes(documents, readJson(join(paths.dotWiki, "config.json"), {}))
|
|
38
42
|
: null;
|
|
39
|
-
const okfLog = vaultState.knowledgeFormat === "okf-0.2" ? eventLogResult
|
|
43
|
+
const okfLog = vaultState.knowledgeFormat === "okf-0.2" ? eventLogResult?.markdown : undefined;
|
|
40
44
|
// Step 8: Atomic write all projections
|
|
41
45
|
mkdirSync(paths.meta, { recursive: true });
|
|
42
46
|
const registryJson = `${JSON.stringify(registry, null, 2)}\n`;
|
|
@@ -44,7 +48,8 @@ export function rebuildMetadata(paths) {
|
|
|
44
48
|
atomicWriteFile(join(paths.meta, "registry.json"), registryJson);
|
|
45
49
|
atomicWriteFile(join(paths.meta, "backlinks.json"), backlinksJson);
|
|
46
50
|
atomicWriteFile(join(paths.meta, "index.md"), metaIndex);
|
|
47
|
-
|
|
51
|
+
if (metaLog !== undefined)
|
|
52
|
+
atomicWriteFile(join(paths.meta, "log.md"), metaLog);
|
|
48
53
|
// Step 9: Write OKF projections if applicable
|
|
49
54
|
if (okfIndexes && okfIndexes.size > 0) {
|
|
50
55
|
mkdirSync(paths.wiki, { recursive: true });
|
|
@@ -54,7 +59,7 @@ export function rebuildMetadata(paths) {
|
|
|
54
59
|
// Prune obsolete generated indexes
|
|
55
60
|
pruneObsoleteIndexes(paths, okfIndexes);
|
|
56
61
|
}
|
|
57
|
-
if (okfLog !==
|
|
62
|
+
if (okfLog !== undefined) {
|
|
58
63
|
mkdirSync(paths.wiki, { recursive: true });
|
|
59
64
|
atomicWriteFile(join(paths.wiki, "log.md"), okfLog);
|
|
60
65
|
}
|
|
@@ -196,25 +201,21 @@ function buildIndexMarkdown(registry) {
|
|
|
196
201
|
sections.push(`---\n*Last updated: ${registry.last_updated}* | *Total pages: ${Object.keys(registry.pages).length}*`);
|
|
197
202
|
return `${sections.join("\n")}\n`;
|
|
198
203
|
}
|
|
199
|
-
|
|
200
|
-
function buildLogMarkdown(paths) {
|
|
201
|
-
const eventsPath = join(paths.meta, "events.jsonl");
|
|
204
|
+
function buildLogMarkdown(eventsJsonl) {
|
|
202
205
|
const events = [];
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
events.push(candidate);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
// skip malformed
|
|
206
|
+
const raw = eventsJsonl.trim();
|
|
207
|
+
for (const line of raw.split("\n")) {
|
|
208
|
+
if (!line.trim())
|
|
209
|
+
continue;
|
|
210
|
+
try {
|
|
211
|
+
const candidate = JSON.parse(line);
|
|
212
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
213
|
+
events.push(candidate);
|
|
216
214
|
}
|
|
217
215
|
}
|
|
216
|
+
catch {
|
|
217
|
+
// Keep backward-compatible rich-log behavior: malformed lines are omitted.
|
|
218
|
+
}
|
|
218
219
|
}
|
|
219
220
|
const lines = [];
|
|
220
221
|
lines.push("# Activity Log\n\n> Auto-generated from meta/events.jsonl. Do not edit manually.\n");
|
|
@@ -230,9 +231,8 @@ function buildLogMarkdown(paths) {
|
|
|
230
231
|
lines.push(`- ${details}`);
|
|
231
232
|
lines.push("");
|
|
232
233
|
}
|
|
233
|
-
if (events.length === 0)
|
|
234
|
+
if (events.length === 0)
|
|
234
235
|
lines.push("_No events recorded yet._\n");
|
|
235
|
-
}
|
|
236
236
|
return `${lines.join("\n")}\n`;
|
|
237
237
|
}
|
|
238
238
|
/** Append an event to events.jsonl. */
|
|
@@ -254,15 +254,21 @@ function atomicWriteFile(path, content) {
|
|
|
254
254
|
writeFileSync(temporary, content, "utf8");
|
|
255
255
|
renameSync(temporary, path);
|
|
256
256
|
}
|
|
257
|
-
|
|
258
|
-
function readText(path) {
|
|
257
|
+
function readEventSource(filePath, diagnosticPath = "meta/events.jsonl") {
|
|
259
258
|
try {
|
|
260
|
-
|
|
261
|
-
return "";
|
|
262
|
-
return readFileSync(path, "utf8");
|
|
259
|
+
return { available: true, content: readFileSync(filePath, "utf8") };
|
|
263
260
|
}
|
|
264
|
-
catch {
|
|
265
|
-
|
|
261
|
+
catch (error) {
|
|
262
|
+
const code = error.code;
|
|
263
|
+
// ENOENT = missing; ENOTDIR/EISDIR = not a regular file (FreeBSD dir hazard); else unreadable
|
|
264
|
+
const diagnosticCode = code === "ENOENT" ? "event_source_missing" : "event_source_unreadable";
|
|
265
|
+
const message = diagnosticCode === "event_source_missing"
|
|
266
|
+
? "Authoritative event source is missing; existing log projections were preserved"
|
|
267
|
+
: "Authoritative event source is unreadable; existing log projections were preserved";
|
|
268
|
+
return {
|
|
269
|
+
available: false,
|
|
270
|
+
diagnostic: okfDiag("warning", diagnosticCode, diagnosticPath, message),
|
|
271
|
+
};
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
274
|
/** Prune obsolete generated indexes in OKF mode. */
|
|
@@ -35,7 +35,6 @@ export function registerWikiModelCommand(pi, runtime) {
|
|
|
35
35
|
const apply = (model) => {
|
|
36
36
|
persistTaskModel(ctx.cwd, model);
|
|
37
37
|
runtime.config = { ...runtime.config, taskModel: model };
|
|
38
|
-
runtime.configLoaded = true;
|
|
39
38
|
const label = formatActiveModelLabel(runtime.config, sessionId);
|
|
40
39
|
ctx.ui.setStatus(MODEL_STATUS_KEY, `🧠 wiki model: ${label}`);
|
|
41
40
|
ctx.ui.notify(`LLM Wiki: background tasks now use ${label}`, "info");
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { TASK_DEFAULTS, loadTaskConfig, noticesEnabled } from "./task-config.js";
|
|
2
2
|
export class Runtime {
|
|
3
3
|
config = { ...TASK_DEFAULTS };
|
|
4
|
-
configLoaded = false;
|
|
5
4
|
/**
|
|
6
5
|
* Extension API handle, attached at registration. Used by `report()` to emit
|
|
7
6
|
* visible completion messages for background actions (issue #77). Optional so
|
|
@@ -15,10 +14,7 @@ export class Runtime {
|
|
|
15
14
|
/** Whether we've already surfaced a model-resolution failure (avoid spam). */
|
|
16
15
|
resolveFailureNotified = false;
|
|
17
16
|
ensureConfig(cwd) {
|
|
18
|
-
if (this.configLoaded)
|
|
19
|
-
return;
|
|
20
17
|
this.config = loadTaskConfig(cwd);
|
|
21
|
-
this.configLoaded = true;
|
|
22
18
|
}
|
|
23
19
|
/** True if a task with this label is currently running. */
|
|
24
20
|
isInFlight(label) {
|
|
@@ -80,7 +80,7 @@ function fileCaptureSource(pi, filePath, signal) {
|
|
|
80
80
|
file_path: filePath,
|
|
81
81
|
format: extractor.format,
|
|
82
82
|
}),
|
|
83
|
-
event: () => ({
|
|
83
|
+
event: () => ({ format: extractor.format }),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
function textCaptureSource(text, title) {
|
|
@@ -67,6 +67,12 @@ function readNamespacedConfig(path) {
|
|
|
67
67
|
if (typeof section.trajectories === "boolean") {
|
|
68
68
|
out.trajectories = section.trajectories;
|
|
69
69
|
}
|
|
70
|
+
const lang = section.synthesisLanguage;
|
|
71
|
+
if (typeof lang === "string" && lang.trim()) {
|
|
72
|
+
const canonical = validateSynthesisLanguage(lang.trim());
|
|
73
|
+
if (canonical)
|
|
74
|
+
out.synthesisLanguage = canonical;
|
|
75
|
+
}
|
|
70
76
|
return out;
|
|
71
77
|
}
|
|
72
78
|
catch {
|
|
@@ -91,6 +97,29 @@ export function parseModelRef(ref) {
|
|
|
91
97
|
return undefined;
|
|
92
98
|
return { provider, id };
|
|
93
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate and canonicalize a BCP 47 language tag for synthesisLanguage (issue #124).
|
|
102
|
+
* Returns the canonical tag, or undefined if invalid or suspicious.
|
|
103
|
+
* Uses Intl.getCanonicalLocales for validation; rejects tags containing
|
|
104
|
+
* newlines, quotes, or other prompt-injection candidates.
|
|
105
|
+
*/
|
|
106
|
+
export function validateSynthesisLanguage(tag) {
|
|
107
|
+
// Reject obvious injection attempts: newlines, quotes, braces, angle brackets
|
|
108
|
+
if (/\n|\r|"|'|<|>|{|}/.test(tag))
|
|
109
|
+
return undefined;
|
|
110
|
+
// Reject if it looks like an instruction (contains "write", "ignore", "translate", etc.)
|
|
111
|
+
const lower = tag.toLowerCase();
|
|
112
|
+
if (/\b(write|ignore|translate|system|prompt|instruction)\b/.test(lower))
|
|
113
|
+
return undefined;
|
|
114
|
+
// Basic BCP 47 pattern: language[-script][-region][-variant]*
|
|
115
|
+
// Must start with 2-3 letter language code
|
|
116
|
+
if (!/^[a-z]{2,3}(?:-[A-Za-z]{1,8})*$/.test(tag))
|
|
117
|
+
return undefined;
|
|
118
|
+
const canonical = Intl.getCanonicalLocales(tag);
|
|
119
|
+
if (canonical.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return canonical[0];
|
|
122
|
+
}
|
|
94
123
|
/**
|
|
95
124
|
* Read a settings JSON file as a plain object, or `{}` when it is absent or
|
|
96
125
|
* corrupt. Reads directly (no `existsSync` pre-check) so there is no
|
|
@@ -327,6 +327,7 @@ export function registerWikiIngest(pi, runtime) {
|
|
|
327
327
|
sourceId: s.id,
|
|
328
328
|
manifest: s.manifest,
|
|
329
329
|
extracted: s.extracted,
|
|
330
|
+
synthesisLanguage: runtime.config.synthesisLanguage,
|
|
330
331
|
});
|
|
331
332
|
if (committed) {
|
|
332
333
|
// Background semantic embeddings (#66): embed the pages this
|
|
@@ -960,6 +961,12 @@ export function registerWikiRebuildMeta(pi, runtime) {
|
|
|
960
961
|
if (!result.ok) {
|
|
961
962
|
return `⚠️ LLM Wiki: rebuild had issues — ${result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; ")}`;
|
|
962
963
|
}
|
|
964
|
+
const warnings = result.diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
|
|
965
|
+
if (warnings.length > 0) {
|
|
966
|
+
return `⚠️ LLM Wiki: metadata rebuilt with warnings — ${warnings
|
|
967
|
+
.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
|
|
968
|
+
.join("; ")}`;
|
|
969
|
+
}
|
|
963
970
|
const registry = readJson(join(paths.meta, "registry.json"), {
|
|
964
971
|
version: "1.0",
|
|
965
972
|
last_updated: "",
|
|
@@ -333,6 +333,12 @@ export function isProtectedPath(absPath, paths) {
|
|
|
333
333
|
reason: "Raw sources are immutable. Use wiki_capture_source to add sources.",
|
|
334
334
|
};
|
|
335
335
|
}
|
|
336
|
+
if (relativePhysicalPath(paths.meta, absPath) === "events.jsonl") {
|
|
337
|
+
return {
|
|
338
|
+
protected: true,
|
|
339
|
+
reason: "Event history is append-only authoritative state. Use wiki_log_event or an owning wiki operation instead.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
336
342
|
if (isPathWithin(paths.meta, absPath)) {
|
|
337
343
|
return {
|
|
338
344
|
protected: true,
|
package/dist/mcp/index.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { McpServer
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
14
15
|
import * as z from "zod/v4";
|
|
15
16
|
import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
16
17
|
import { createExecApi } from "./exec.js";
|
package/docs/api.md
CHANGED
|
@@ -280,6 +280,8 @@ Health is `"⚠️ Warning"` when orphan count exceeds 5, `"🔴 Empty"` when th
|
|
|
280
280
|
Force a full synchronous rebuild of all generated metadata: `registry.json`, `backlinks.json`,
|
|
281
281
|
`index.md`, `log.md`. Use when metadata appears out of sync with actual wiki files.
|
|
282
282
|
|
|
283
|
+
If `meta/events.jsonl` is missing or unreadable, rebuild reports a warning and preserves existing log projections while continuing to rebuild registry, backlinks, and indexes. A present zero-byte event file is an intentional empty history.
|
|
284
|
+
|
|
283
285
|
**Parameters**
|
|
284
286
|
|
|
285
287
|
None.
|
|
@@ -294,8 +296,9 @@ details: { pageCount: number }
|
|
|
294
296
|
|
|
295
297
|
## wiki_log_event
|
|
296
298
|
|
|
297
|
-
Append a structured event to `meta/events.jsonl` and regenerate
|
|
298
|
-
|
|
299
|
+
Append a structured event to the authoritative, append-only `meta/events.jsonl` stream and regenerate available log projections. Every event is timestamped automatically. The event stream must be preserved in full-vault backups; generated Markdown logs cannot reconstruct it.
|
|
300
|
+
|
|
301
|
+
`details` is user-controlled and may appear in OKF-mode `wiki/log.md`. Do not include secrets, credentials, or private machine-local paths. Built-in local-file capture records its stable source ID and format in events, omitting the caller-supplied input path from the event stream and generated log projections. The exact path remains in the extension-owned raw manifest. Other wiki projections are not covered by this guarantee: extraction-failure text and source-page titles may still surface path-derived strings, so treat the source page area as potentially machine-local until a dedicated sanitization pass exists.
|
|
299
302
|
|
|
300
303
|
**Parameters**
|
|
301
304
|
|
package/docs/architecture.md
CHANGED
|
@@ -55,7 +55,7 @@ WIKI_ROOT/
|
|
|
55
55
|
│ ├── analyses/ # Durable query answers
|
|
56
56
|
│ ├── cases/ # One specific past task per trajectory
|
|
57
57
|
│ └── skills/ # Reusable patterns distilled from trajectories
|
|
58
|
-
├── meta/ #
|
|
58
|
+
├── meta/ # Durable event source + generated internal projections
|
|
59
59
|
│ ├── registry.json # Master page catalog
|
|
60
60
|
│ ├── backlinks.json # Inbound link map
|
|
61
61
|
│ ├── index.md # Human-readable catalog
|
|
@@ -73,9 +73,12 @@ WIKI_ROOT/
|
|
|
73
73
|
| --------------------- | ------------------------ | ------------------------ |
|
|
74
74
|
| `.llm-wiki/raw/**` | Extension | Immutable after capture |
|
|
75
75
|
| `.llm-wiki/wiki/**` | Model + user | Editable knowledge pages |
|
|
76
|
-
| `.llm-wiki/meta
|
|
76
|
+
| `.llm-wiki/meta/events.jsonl` | Extension tools | Authoritative, append-only; preserve in full-vault backups |
|
|
77
|
+
| `.llm-wiki/meta/**` except `events.jsonl` | Extension | Generated projections |
|
|
77
78
|
| `.llm-wiki/` | Human + explicit request | Operating rules |
|
|
78
79
|
|
|
80
|
+
`events.jsonl` records selected extension operations, not every filesystem edit. `meta/log.md` and OKF-mode `wiki/log.md` are one-way projections; neither can recover the event stream.
|
|
81
|
+
|
|
79
82
|
## Source Packet Format
|
|
80
83
|
|
|
81
84
|
Each captured source becomes a packet:
|
package/docs/configuration.md
CHANGED
|
@@ -32,6 +32,31 @@ The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always avail
|
|
|
32
32
|
| `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
|
|
33
33
|
| `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
|
|
34
34
|
|
|
35
|
+
## Pi Agent Settings
|
|
36
|
+
|
|
37
|
+
Runtime settings for the wiki's background tasks live in `.pi/settings.json` under the `llm-wiki` namespace. These can be set globally (`~/.pi/agent/settings.json`) or per-project (`<cwd>/.pi/settings.json`).
|
|
38
|
+
|
|
39
|
+
| Setting | Default | Description |
|
|
40
|
+
| --------------------- | ------- | ------------------------------------------------------------ |
|
|
41
|
+
| `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
|
|
42
|
+
| `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
|
|
43
|
+
| `trajectories` | false | Enable agent-trajectory working-memory |
|
|
44
|
+
| `notices` | true | Show wiki activity notices in chat |
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"llm-wiki": {
|
|
51
|
+
"synthesisLanguage": "ru",
|
|
52
|
+
"taskModel": {
|
|
53
|
+
"provider": "openai",
|
|
54
|
+
"id": "gpt-4o"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
35
60
|
## Vault Resolution
|
|
36
61
|
|
|
37
62
|
The vault root is resolved in this priority order:
|