@zosmaai/pi-llm-wiki 0.11.4 → 0.11.6
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/README.de.md +8 -0
- package/README.es.md +8 -0
- package/README.fr.md +8 -0
- package/README.hi.md +8 -0
- package/README.ja.md +8 -0
- package/README.ko.md +8 -0
- package/README.md +8 -0
- package/README.pt.md +8 -0
- package/README.ru.md +8 -0
- package/README.zh.md +8 -0
- package/assets/wiki-dashboard.png +0 -0
- package/commands/wiki-ingest.md +1 -0
- package/commands/wiki-req.md +1 -0
- package/commands/wiki-retro.md +1 -0
- package/dist/extensions/llm-wiki/lib/dashboard-command.js +86 -0
- package/dist/extensions/llm-wiki/lib/dashboard.js +175 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +30 -1
- package/dist/extensions/llm-wiki/lib/host.js +21 -1
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +44 -20
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +20 -2
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +133 -27
- package/dist/extensions/llm-wiki/lib/metadata.js +6 -6
- package/dist/extensions/llm-wiki/lib/observation.js +22 -3
- package/dist/extensions/llm-wiki/lib/retro.js +38 -4
- package/dist/extensions/llm-wiki/lib/runtime.js +2 -2
- package/dist/extensions/llm-wiki/lib/settings-command.js +377 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +100 -1
- package/dist/extensions/llm-wiki/lib/tools.js +47 -8
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/operations.js +21 -2
- package/docs/api.md +24 -1
- package/docs/commands.md +6 -1
- package/docs/configuration.md +11 -0
- package/docs/obsidian.md +6 -6
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-1-quality-baseline-and-compatibility.md +1520 -0
- package/docs/superpowers/plans/2026-08-27-wikilink-resolver-normalization.md +735 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-gate-ensure-page-retro.md +642 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-write-validation.md +695 -0
- package/docs/superpowers/roadmaps/2026-08-09-qmd-retrieval-roadmap.md +448 -0
- package/docs/superpowers/specs/2026-08-08-qmd-retrieval-design.md +806 -0
- package/extensions/llm-wiki/index.ts +4 -0
- package/extensions/llm-wiki/lib/dashboard-command.ts +106 -0
- package/extensions/llm-wiki/lib/dashboard.ts +210 -0
- package/extensions/llm-wiki/lib/guardrails.ts +26 -1
- package/extensions/llm-wiki/lib/host.ts +21 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +64 -27
- package/extensions/llm-wiki/lib/knowledge-document.ts +21 -2
- package/extensions/llm-wiki/lib/knowledge-links.ts +208 -35
- package/extensions/llm-wiki/lib/metadata.ts +10 -6
- package/extensions/llm-wiki/lib/observation.ts +23 -3
- package/extensions/llm-wiki/lib/retro.ts +48 -4
- package/extensions/llm-wiki/lib/runtime.ts +2 -2
- package/extensions/llm-wiki/lib/settings-command.ts +483 -0
- package/extensions/llm-wiki/lib/task-config.ts +138 -0
- package/extensions/llm-wiki/lib/tools.ts +62 -8
- package/mcp/index.ts +12 -1
- package/mcp/operations.ts +32 -2
- package/package.json +4 -4
- package/prompts/wiki-ingest.md +1 -0
- package/prompts/wiki-req.md +1 -0
- package/prompts/wiki-retro.md +1 -0
- package/skills/llm-wiki/SKILL.md +11 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { Container, Input, SettingsList, Spacer, Text, } from "@mariozechner/pi-tui";
|
|
3
|
+
import { loadTaskConfig, loadTaskConfigSources, parseModelRef, persistSetting, trajectoriesEnabled, } from "./task-config.js";
|
|
4
|
+
const SETTINGS = [
|
|
5
|
+
{
|
|
6
|
+
key: "taskModel",
|
|
7
|
+
label: "Model",
|
|
8
|
+
type: "model",
|
|
9
|
+
hint: "Model for background wiki tasks (provider/id).",
|
|
10
|
+
menuLabel: "Task model — provider/id, empty = session model",
|
|
11
|
+
format: (v) => {
|
|
12
|
+
const m = v;
|
|
13
|
+
return m ? `${m.provider}/${m.id}` : "(session model)";
|
|
14
|
+
},
|
|
15
|
+
toEdit: (v) => {
|
|
16
|
+
const m = v;
|
|
17
|
+
return m ? `${m.provider}/${m.id}` : "";
|
|
18
|
+
},
|
|
19
|
+
parse: (input) => parseModelRef(input),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "synthesisMaxTokens",
|
|
23
|
+
label: "Synthesis Tokens",
|
|
24
|
+
type: "number",
|
|
25
|
+
hint: "Max output tokens for wiki synthesis runs.",
|
|
26
|
+
defaultText: "16384",
|
|
27
|
+
format: (v) => (v != null ? String(v) : "16384 (default)"),
|
|
28
|
+
toEdit: (v) => (v != null ? String(v) : "16384"),
|
|
29
|
+
parse: (input) => {
|
|
30
|
+
const n = Number(input);
|
|
31
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Math.floor(n);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "trajectories",
|
|
38
|
+
label: "Trajectories",
|
|
39
|
+
type: "boolean",
|
|
40
|
+
hint: "Capture agent trajectories into the vault.",
|
|
41
|
+
format: (v) => (v ? "ON" : "OFF"),
|
|
42
|
+
toEdit: () => "",
|
|
43
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "notices",
|
|
47
|
+
label: "Notices",
|
|
48
|
+
type: "boolean",
|
|
49
|
+
hint: "Show wiki recall/observation notice lines in chat.",
|
|
50
|
+
format: (v) => (v != null ? (v ? "ON" : "OFF") : "ON (default)"),
|
|
51
|
+
toEdit: () => "",
|
|
52
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "ambientPersonalVault",
|
|
56
|
+
label: "Ambient Personal",
|
|
57
|
+
type: "boolean",
|
|
58
|
+
hint: "Include the personal vault in ambient session context.",
|
|
59
|
+
format: (v) => (v != null ? (v ? "ON" : "OFF") : "host-dependent"),
|
|
60
|
+
toEdit: () => "",
|
|
61
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "synthesisLanguage",
|
|
65
|
+
label: "Synthesis Language",
|
|
66
|
+
type: "string",
|
|
67
|
+
hint: "Language for synthesized wiki content.",
|
|
68
|
+
defaultText: "en",
|
|
69
|
+
format: (v) => (v ? String(v) : "en (default)"),
|
|
70
|
+
toEdit: (v) => (v ? String(v) : "en"),
|
|
71
|
+
parse: (input) => {
|
|
72
|
+
const trimmed = input.trim();
|
|
73
|
+
return trimmed || undefined;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "semanticWeight",
|
|
78
|
+
label: "Semantic Weight",
|
|
79
|
+
type: "number",
|
|
80
|
+
hint: "0–1 weight for embedding (semantic) recall.",
|
|
81
|
+
defaultText: "0.5",
|
|
82
|
+
format: (v) => (v != null ? String(v) : "0.5 (default)"),
|
|
83
|
+
toEdit: (v) => (v != null ? String(v) : "0.5"),
|
|
84
|
+
parse: (input) => {
|
|
85
|
+
const n = Number(input);
|
|
86
|
+
if (!Number.isFinite(n) || n < 0 || n > 1)
|
|
87
|
+
return undefined;
|
|
88
|
+
return n;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "recallLinksThreshold",
|
|
93
|
+
label: "Recall Links",
|
|
94
|
+
type: "number",
|
|
95
|
+
hint: "Max recall links shown when the vault is large.",
|
|
96
|
+
defaultText: "50",
|
|
97
|
+
format: (v) => (v != null ? String(v) : "50 (default)"),
|
|
98
|
+
toEdit: (v) => (v != null ? String(v) : "50"),
|
|
99
|
+
parse: (input) => {
|
|
100
|
+
const n = Number(input);
|
|
101
|
+
if (!Number.isFinite(n) || n < 0)
|
|
102
|
+
return undefined;
|
|
103
|
+
return Math.floor(n);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: "recallSkillInlineMax",
|
|
108
|
+
label: "Skill Inline Max",
|
|
109
|
+
type: "number",
|
|
110
|
+
hint: "Max characters inlined from recall into skill prompts.",
|
|
111
|
+
defaultText: "1600",
|
|
112
|
+
format: (v) => (v != null ? String(v) : "1600 (default)"),
|
|
113
|
+
toEdit: (v) => (v != null ? String(v) : "1600"),
|
|
114
|
+
parse: (input) => {
|
|
115
|
+
const n = Number(input);
|
|
116
|
+
if (!Number.isFinite(n) || n < 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
return Math.floor(n);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "embeddingProvider",
|
|
123
|
+
label: "Embedding Provider",
|
|
124
|
+
type: "string",
|
|
125
|
+
hint: "Embedding provider (e.g. openai). Empty = embeddings disabled.",
|
|
126
|
+
format: (v) => (v ? String(v) : "(disabled)"),
|
|
127
|
+
toEdit: (v) => (v ? String(v) : ""),
|
|
128
|
+
parse: (input) => input.trim() || undefined,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: "embeddingModel",
|
|
132
|
+
label: "Embedding Model",
|
|
133
|
+
type: "string",
|
|
134
|
+
hint: "Embedding model id.",
|
|
135
|
+
defaultText: "text-embedding-3-small",
|
|
136
|
+
format: (v) => (v ? String(v) : "text-embedding-3-small (default)"),
|
|
137
|
+
toEdit: (v) => (v ? String(v) : "text-embedding-3-small"),
|
|
138
|
+
parse: (input) => input.trim() || undefined,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: "embeddingBaseUrl",
|
|
142
|
+
label: "Embedding Base URL",
|
|
143
|
+
type: "string",
|
|
144
|
+
hint: "Custom embeddings API base URL (optional).",
|
|
145
|
+
format: (v) => (v ? String(v) : "—"),
|
|
146
|
+
toEdit: (v) => (v ? String(v) : ""),
|
|
147
|
+
parse: (input) => input.trim() || undefined,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "embeddingApiKeyEnv",
|
|
151
|
+
label: "Embedding API Key Env",
|
|
152
|
+
type: "string",
|
|
153
|
+
hint: "Environment variable name holding the embeddings API key.",
|
|
154
|
+
defaultText: "OPENAI_API_KEY",
|
|
155
|
+
format: (v) => (v ? String(v) : "OPENAI_API_KEY (default)"),
|
|
156
|
+
toEdit: (v) => (v ? String(v) : "OPENAI_API_KEY"),
|
|
157
|
+
parse: (input) => input.trim() || undefined,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
function isInsideHome(cwd) {
|
|
161
|
+
return cwd.startsWith(homedir());
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Map effective settings + sources to pi-tui SettingItems.
|
|
165
|
+
*
|
|
166
|
+
* Exported for tests. `notify` is used by edit menus to report invalid input
|
|
167
|
+
* without closing the menu.
|
|
168
|
+
*/
|
|
169
|
+
export function buildSettingItems(sources, notify) {
|
|
170
|
+
return SETTINGS.map((def) => {
|
|
171
|
+
const entry = sources[def.key];
|
|
172
|
+
const value = entry?.value;
|
|
173
|
+
const source = entry?.source ?? "default";
|
|
174
|
+
const item = {
|
|
175
|
+
id: def.key,
|
|
176
|
+
label: def.label,
|
|
177
|
+
currentValue: def.format(value),
|
|
178
|
+
description: describeSetting(def, source),
|
|
179
|
+
};
|
|
180
|
+
if (def.type === "boolean") {
|
|
181
|
+
item.values = ["OFF", "ON"];
|
|
182
|
+
return item;
|
|
183
|
+
}
|
|
184
|
+
item.submenu = (_display, done) => {
|
|
185
|
+
const sub = new InputSubmenu(def.menuLabel ?? `Set ${def.label}`, def.toEdit(value));
|
|
186
|
+
sub.input.onSubmit = (raw) => {
|
|
187
|
+
const trimmed = raw.trim();
|
|
188
|
+
if (def.type === "model") {
|
|
189
|
+
if (!trimmed) {
|
|
190
|
+
done(""); // clear → back to session model
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const ref = parseModelRef(trimmed);
|
|
194
|
+
if (!ref) {
|
|
195
|
+
notify(`LLM Wiki: could not parse "${trimmed}". Use provider/id.`, "error");
|
|
196
|
+
return; // stay in the menu
|
|
197
|
+
}
|
|
198
|
+
done(`${ref.provider}/${ref.id}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!trimmed) {
|
|
202
|
+
done(); // empty = no change
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const parsed = def.parse(trimmed);
|
|
206
|
+
if (parsed === undefined) {
|
|
207
|
+
notify(`LLM Wiki: invalid value "${trimmed}" for ${def.label}`, "error");
|
|
208
|
+
return; // stay in the menu
|
|
209
|
+
}
|
|
210
|
+
done(String(parsed));
|
|
211
|
+
};
|
|
212
|
+
sub.input.onEscape = () => done();
|
|
213
|
+
return sub;
|
|
214
|
+
};
|
|
215
|
+
return item;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function describeSetting(def, source) {
|
|
219
|
+
const where = source === "default" ? "Not set — using default." : `Set in ${source} settings.`;
|
|
220
|
+
return `${def.hint}\n${where}`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Parse the display string a SettingItem cycled/edited into back into the
|
|
224
|
+
* typed value to persist.
|
|
225
|
+
*/
|
|
226
|
+
function parseDisplay(def, display) {
|
|
227
|
+
if (def.type === "boolean")
|
|
228
|
+
return display === "ON";
|
|
229
|
+
if (def.type === "model")
|
|
230
|
+
return display ? parseModelRef(display) : undefined;
|
|
231
|
+
return def.parse(display);
|
|
232
|
+
}
|
|
233
|
+
function buildSettingsListTheme(theme) {
|
|
234
|
+
return {
|
|
235
|
+
label: (text, selected) => (selected ? theme.fg("accent", text) : text),
|
|
236
|
+
value: (text, selected) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)),
|
|
237
|
+
description: (text) => theme.fg("dim", text),
|
|
238
|
+
cursor: theme.fg("accent", "→ "),
|
|
239
|
+
hint: (text) => theme.fg("dim", text),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Title + single-line input. SettingsList forwards all key input here. */
|
|
243
|
+
class InputSubmenu extends Container {
|
|
244
|
+
input;
|
|
245
|
+
constructor(label, initialValue) {
|
|
246
|
+
super();
|
|
247
|
+
this.addChild(new Text(label, 0, 0));
|
|
248
|
+
this.input = new Input();
|
|
249
|
+
// Type the prefill instead of setValue(): setValue keeps the cursor at 0,
|
|
250
|
+
// which makes backspace no-op and typed input land before the prefill.
|
|
251
|
+
if (initialValue)
|
|
252
|
+
this.input.handleInput(initialValue);
|
|
253
|
+
this.addChild(this.input);
|
|
254
|
+
}
|
|
255
|
+
handleInput(data) {
|
|
256
|
+
this.input.handleInput(data);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Header + settings list. ui.custom gives this component focus. */
|
|
260
|
+
class SettingsScreen extends Container {
|
|
261
|
+
list;
|
|
262
|
+
titleText;
|
|
263
|
+
constructor(title, list) {
|
|
264
|
+
super();
|
|
265
|
+
this.list = list;
|
|
266
|
+
this.titleText = new Text(title, 0, 0);
|
|
267
|
+
this.addChild(this.titleText);
|
|
268
|
+
this.addChild(new Spacer(1));
|
|
269
|
+
this.addChild(list);
|
|
270
|
+
}
|
|
271
|
+
handleInput(data) {
|
|
272
|
+
this.list.handleInput(data);
|
|
273
|
+
}
|
|
274
|
+
/** Update the header line in place (scope changes re-target it live). */
|
|
275
|
+
setTitle(title) {
|
|
276
|
+
this.titleText.setText(title);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Build the header title for the scope currently being written to. */
|
|
280
|
+
function scopeTitle(scope) {
|
|
281
|
+
return `\u{1F9E0} LLM Wiki Settings \u2014 ${scope === "global" ? "Global" : "Project"} (Esc to close)`;
|
|
282
|
+
}
|
|
283
|
+
async function showSettingsTui(ui, cwd, scope) {
|
|
284
|
+
if (typeof ui.custom !== "function") {
|
|
285
|
+
ui.notify("LLM Wiki: this host does not support the settings screen. Edit settings.json directly.", "warning");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// The editable Scope row re-targets where subsequent writes land; the
|
|
289
|
+
// picker / inside-home heuristic only chooses the starting value.
|
|
290
|
+
let writeScope = scope;
|
|
291
|
+
const scopeItem = {
|
|
292
|
+
id: "scope",
|
|
293
|
+
label: "Scope",
|
|
294
|
+
currentValue: writeScope === "global" ? "Global" : "Project",
|
|
295
|
+
values: ["Global", "Project"],
|
|
296
|
+
description: "Where edits are written. Global \u2192 ~/.pi/agent/settings.json \u00b7 Project \u2192 .pi/settings.json (this folder).",
|
|
297
|
+
};
|
|
298
|
+
const items = [scopeItem, ...buildSettingItems(loadTaskConfigSources(cwd), ui.notify.bind(ui))];
|
|
299
|
+
// Tracks the effective value so the reload note fires on actual changes.
|
|
300
|
+
let prevTrajectories = trajectoriesEnabled(loadTaskConfig(cwd));
|
|
301
|
+
let list;
|
|
302
|
+
let screenRef;
|
|
303
|
+
await ui.custom((_tui, theme, _keybindings, close) => {
|
|
304
|
+
list = new SettingsList(items, items.length, buildSettingsListTheme(theme), (id, display) => {
|
|
305
|
+
if (!list)
|
|
306
|
+
return;
|
|
307
|
+
// Scope row: re-target writes; nothing is persisted for it.
|
|
308
|
+
if (id === "scope") {
|
|
309
|
+
writeScope = display === "Global" ? "global" : "project";
|
|
310
|
+
list.updateValue("scope", display);
|
|
311
|
+
screenRef?.setTitle(scopeTitle(writeScope));
|
|
312
|
+
ui.notify(`LLM Wiki: writing to ${writeScope === "global"
|
|
313
|
+
? "Global (~/.pi/agent/settings.json)"
|
|
314
|
+
: "Project (.pi/settings.json)"}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const def = SETTINGS.find((d) => d.key === id);
|
|
318
|
+
if (!def)
|
|
319
|
+
return;
|
|
320
|
+
const value = parseDisplay(def, display);
|
|
321
|
+
// Non-model, non-boolean values are validated before done(); undefined
|
|
322
|
+
// here is only expected for the model-clear case.
|
|
323
|
+
if (value === undefined && def.type !== "model")
|
|
324
|
+
return;
|
|
325
|
+
try {
|
|
326
|
+
persistSetting(cwd, writeScope, def.key, value);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
ui.notify(`LLM Wiki: failed to save ${def.label}: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Normalize the displayed value in place (e.g. cleared model shows
|
|
333
|
+
// "(session model)"; "0.50" becomes "0.5").
|
|
334
|
+
list.updateValue(id, def.format(value));
|
|
335
|
+
// trajectories gates tool registration at startup, so a live toggle
|
|
336
|
+
// cannot add/remove the 3 tools mid-session \u2014 say so on real changes.
|
|
337
|
+
if (def.key === "trajectories") {
|
|
338
|
+
const next = Boolean(value);
|
|
339
|
+
if (next !== prevTrajectories) {
|
|
340
|
+
prevTrajectories = next;
|
|
341
|
+
ui.notify("LLM Wiki: trajectory tools register at startup \u2014 new value applies after a reload");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}, () => close());
|
|
345
|
+
screenRef = new SettingsScreen(scopeTitle(writeScope), list);
|
|
346
|
+
return screenRef;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Register the /wiki-settings command.
|
|
351
|
+
*/
|
|
352
|
+
export function registerWikiSettingsCommand(pi, runtime) {
|
|
353
|
+
pi.registerCommand("wiki-settings", {
|
|
354
|
+
description: "View and edit LLM Wiki settings (model, tokens, behaviors, embeddings)",
|
|
355
|
+
handler: async (_args, ctx) => {
|
|
356
|
+
runtime.ensureConfig(ctx.cwd);
|
|
357
|
+
if (!ctx.hasUI) {
|
|
358
|
+
ctx.ui.notify("LLM Wiki: /wiki-settings requires an interactive UI. Edit settings.json directly instead.", "warning");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
let scope;
|
|
362
|
+
if (isInsideHome(ctx.cwd)) {
|
|
363
|
+
scope = "global";
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const pick = await ctx.ui.select("LLM Wiki \u2014 Settings scope?", [
|
|
367
|
+
`Project (${ctx.cwd}/.pi/settings.json)`,
|
|
368
|
+
"Global (~/.pi/agent/settings.json)",
|
|
369
|
+
]);
|
|
370
|
+
if (pick === undefined)
|
|
371
|
+
return;
|
|
372
|
+
scope = pick.startsWith("Project") ? "project" : "global";
|
|
373
|
+
}
|
|
374
|
+
await showSettingsTui(ctx.ui, ctx.cwd, scope);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { detectHost, listGlobalSettingsFiles, listProjectSettingsFiles, resolveProjectSettingsPath, } from "./host.js";
|
|
4
|
+
import { detectHost, listGlobalSettingsFiles, listProjectSettingsFiles, resolveGlobalSettingsPath, resolveProjectSettingsPath, } from "./host.js";
|
|
5
5
|
export const TASK_DEFAULTS = {};
|
|
6
6
|
/**
|
|
7
7
|
* Resolve whether user-facing wiki notices are enabled (issue #77). Defaults
|
|
@@ -10,6 +10,23 @@ export const TASK_DEFAULTS = {};
|
|
|
10
10
|
export function noticesEnabled(config) {
|
|
11
11
|
return config?.notices !== false;
|
|
12
12
|
}
|
|
13
|
+
const WIKILINK_VALIDATION_MODES = [
|
|
14
|
+
"off",
|
|
15
|
+
"warn",
|
|
16
|
+
"strict",
|
|
17
|
+
"normalize",
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the wikilink write-gate mode (issue #172, Layer 2). Defaults to
|
|
21
|
+
* `warn` — ingest always writes and reports; only an explicit `strict` blocks.
|
|
22
|
+
* Unknown values fall back to `warn` rather than failing the ingest.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveWikilinkValidation(config) {
|
|
25
|
+
const v = config?.wikilinkValidation;
|
|
26
|
+
if (v && WIKILINK_VALIDATION_MODES.includes(v))
|
|
27
|
+
return v;
|
|
28
|
+
return "warn";
|
|
29
|
+
}
|
|
13
30
|
/**
|
|
14
31
|
* Resolve whether the personal vault may serve as this project's ambient
|
|
15
32
|
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
@@ -87,6 +104,14 @@ function readNamespacedConfig(path) {
|
|
|
87
104
|
if (canonical)
|
|
88
105
|
out.synthesisLanguage = canonical;
|
|
89
106
|
}
|
|
107
|
+
const maxTokens = section.synthesisMaxTokens;
|
|
108
|
+
if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
109
|
+
out.synthesisMaxTokens = Math.floor(maxTokens);
|
|
110
|
+
}
|
|
111
|
+
const wl = section.wikilinkValidation;
|
|
112
|
+
if (typeof wl === "string" && WIKILINK_VALIDATION_MODES.includes(wl)) {
|
|
113
|
+
out.wikilinkValidation = wl;
|
|
114
|
+
}
|
|
90
115
|
return out;
|
|
91
116
|
}
|
|
92
117
|
catch {
|
|
@@ -156,6 +181,19 @@ function readSettingsObject(path) {
|
|
|
156
181
|
}
|
|
157
182
|
return {};
|
|
158
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Rewrite the `llm-wiki` section of the global settings file.
|
|
186
|
+
*/
|
|
187
|
+
function updateGlobalSection(mutate) {
|
|
188
|
+
const settingsPath = resolveGlobalSettingsPath();
|
|
189
|
+
const raw = readSettingsObject(settingsPath);
|
|
190
|
+
const existing = raw[SETTINGS_KEY];
|
|
191
|
+
const section = existing && typeof existing === "object" ? { ...existing } : {};
|
|
192
|
+
mutate(section);
|
|
193
|
+
raw[SETTINGS_KEY] = section;
|
|
194
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
195
|
+
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
196
|
+
}
|
|
159
197
|
/**
|
|
160
198
|
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
161
199
|
* other top-level key and every other setting in the section.
|
|
@@ -224,3 +262,64 @@ export function loadTaskConfig(cwd) {
|
|
|
224
262
|
}
|
|
225
263
|
return config;
|
|
226
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolve where each setting is defined: project > global > default.
|
|
267
|
+
*/
|
|
268
|
+
/** All known setting keys — needed because TASK_DEFAULTS is {} (zero-config). */
|
|
269
|
+
const KNOWN_KEYS = [
|
|
270
|
+
"taskModel",
|
|
271
|
+
"embeddingProvider",
|
|
272
|
+
"embeddingModel",
|
|
273
|
+
"embeddingBaseUrl",
|
|
274
|
+
"embeddingApiKey",
|
|
275
|
+
"embeddingApiKeyEnv",
|
|
276
|
+
"semanticWeight",
|
|
277
|
+
"recallLinksThreshold",
|
|
278
|
+
"recallSkillInlineMax",
|
|
279
|
+
"notices",
|
|
280
|
+
"ambientPersonalVault",
|
|
281
|
+
"trajectories",
|
|
282
|
+
"synthesisLanguage",
|
|
283
|
+
"synthesisMaxTokens",
|
|
284
|
+
"wikilinkValidation",
|
|
285
|
+
];
|
|
286
|
+
export function loadTaskConfigSources(cwd) {
|
|
287
|
+
const globalResult = {};
|
|
288
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
289
|
+
Object.assign(globalResult, readNamespacedConfig(path));
|
|
290
|
+
}
|
|
291
|
+
const projectResult = {};
|
|
292
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
293
|
+
Object.assign(projectResult, readNamespacedConfig(path));
|
|
294
|
+
}
|
|
295
|
+
const effective = loadTaskConfig(cwd);
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const key of KNOWN_KEYS) {
|
|
298
|
+
if (key in projectResult)
|
|
299
|
+
out[key] = { value: projectResult[key], source: "project" };
|
|
300
|
+
else if (key in globalResult)
|
|
301
|
+
out[key] = { value: globalResult[key], source: "global" };
|
|
302
|
+
else
|
|
303
|
+
out[key] = { value: effective[key], source: "default" };
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Generic setting persist: writes any single setting to the chosen scope.
|
|
309
|
+
*/
|
|
310
|
+
export function persistSetting(cwd, scope, key, value) {
|
|
311
|
+
const mutate = (section) => {
|
|
312
|
+
if (value === undefined || value === null) {
|
|
313
|
+
delete section[key];
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
section[key] = value;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
if (scope === "project") {
|
|
320
|
+
updateProjectSection(cwd, mutate);
|
|
321
|
+
}
|
|
322
|
+
else if (scope === "global") {
|
|
323
|
+
updateGlobalSection(mutate);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -6,11 +6,11 @@ import { launchEmbedPages, reindexEmbeddings, resolveEmbedder } from "./embeddin
|
|
|
6
6
|
import { scheduleReindex } from "./indexing.js";
|
|
7
7
|
import { runIngestSynthesis } from "./ingest-worker.js";
|
|
8
8
|
import { createKnowledgeDocument, serializeKnowledgeDocument, writeKnowledgeDocumentFile, } from "./knowledge-document.js";
|
|
9
|
-
import { buildResolvedBacklinks } from "./knowledge-links.js";
|
|
9
|
+
import { applyWikilinkGate, buildResolvedBacklinks, buildWikilinkIndex, } from "./knowledge-links.js";
|
|
10
10
|
import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
|
|
11
11
|
import { appendEvent, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
|
|
12
12
|
import { captureFile, captureText, captureUrl } from "./source-packet.js";
|
|
13
|
-
import { parseModelRef } from "./task-config.js";
|
|
13
|
+
import { loadTaskConfig, parseModelRef, resolveWikilinkValidation } from "./task-config.js";
|
|
14
14
|
import { detectVaultFormat, fmtDate, getVaultPaths, readJson, resolveVaultPaths, slugify, writeJson, } from "./utils.js";
|
|
15
15
|
import { assertWritableVault, compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, inspectWritableVault, } from "./vault-format.js";
|
|
16
16
|
import { getWikiStatus, searchRegistry } from "./wiki-service.js";
|
|
@@ -328,6 +328,7 @@ export function registerWikiIngest(pi, runtime) {
|
|
|
328
328
|
manifest: s.manifest,
|
|
329
329
|
extracted: s.extracted,
|
|
330
330
|
synthesisLanguage: runtime.config.synthesisLanguage,
|
|
331
|
+
wikilinkValidation: runtime.config.wikilinkValidation,
|
|
331
332
|
});
|
|
332
333
|
if (committed) {
|
|
333
334
|
// Background semantic embeddings (#66): embed the pages this
|
|
@@ -341,8 +342,10 @@ export function registerWikiIngest(pi, runtime) {
|
|
|
341
342
|
];
|
|
342
343
|
launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
|
|
343
344
|
}
|
|
345
|
+
const wl = committed?.wikilinkDiagnostics?.length ?? 0;
|
|
346
|
+
const wlNote = wl > 0 ? `, ${wl} wikilink issue${wl === 1 ? "" : "s"}` : "";
|
|
344
347
|
const summary = committed
|
|
345
|
-
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
|
|
348
|
+
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}${wlNote}`
|
|
346
349
|
: `LLM Wiki: ${s.id} produced no synthesis`;
|
|
347
350
|
if (ctx.hasUI) {
|
|
348
351
|
ctx.ui.notify(summary, committed ? "info" : "warning");
|
|
@@ -459,7 +462,31 @@ export function registerWikiEnsurePage(pi, runtime) {
|
|
|
459
462
|
};
|
|
460
463
|
}
|
|
461
464
|
const today = fmtDate();
|
|
462
|
-
|
|
465
|
+
let body = params.content ?? buildPageBody(type, params.title);
|
|
466
|
+
// Pre-write wikilink gate (#172): validate/normalize caller-supplied content.
|
|
467
|
+
const mode = resolveWikilinkValidation(loadTaskConfig(ctx.cwd));
|
|
468
|
+
let wikilinkIssues = [];
|
|
469
|
+
if (mode !== "off") {
|
|
470
|
+
const registry = readJson(join(paths.meta, "registry.json"), { pages: {} });
|
|
471
|
+
const gate = applyWikilinkGate(body, buildWikilinkIndex(Object.keys(registry.pages)), `${folder}/${slug}`, mode);
|
|
472
|
+
wikilinkIssues = gate.diagnostics.map((d) => d.message);
|
|
473
|
+
if (!gate.ok) {
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: "text",
|
|
478
|
+
text: `Rejected write — unresolved/ambiguous wikilinks:\n${wikilinkIssues
|
|
479
|
+
.map((m) => `- ${m}`)
|
|
480
|
+
.join("\n")}`,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
details: { error: "link_validation", issues: wikilinkIssues },
|
|
484
|
+
isError: true,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (mode === "normalize")
|
|
488
|
+
body = gate.body;
|
|
489
|
+
}
|
|
463
490
|
const doc = createKnowledgeDocument(`${folder}/${slug}.md`, {
|
|
464
491
|
type,
|
|
465
492
|
title: params.title,
|
|
@@ -483,9 +510,16 @@ export function registerWikiEnsurePage(pi, runtime) {
|
|
|
483
510
|
else {
|
|
484
511
|
rebuildMetadataLight(paths);
|
|
485
512
|
}
|
|
513
|
+
const gateNote = wikilinkIssues.length
|
|
514
|
+
? `\n\n⚠️ ${wikilinkIssues.length} wikilink issue(s):\n${wikilinkIssues.map((m) => `- ${m}`).join("\n")}`
|
|
515
|
+
: "";
|
|
486
516
|
return {
|
|
487
|
-
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}
|
|
488
|
-
details: {
|
|
517
|
+
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`${gateNote}` }],
|
|
518
|
+
details: {
|
|
519
|
+
path: pagePath,
|
|
520
|
+
created: true,
|
|
521
|
+
wikilinkIssues,
|
|
522
|
+
},
|
|
489
523
|
};
|
|
490
524
|
},
|
|
491
525
|
});
|
|
@@ -740,14 +774,14 @@ function runWikiLint(paths, autoFix) {
|
|
|
740
774
|
}
|
|
741
775
|
const discovery = discoverKnowledgeDocuments(paths);
|
|
742
776
|
const pages = discovery.documents;
|
|
743
|
-
const
|
|
777
|
+
const wikilinkIndex = buildWikilinkIndex(pages.map((page) => page.id));
|
|
744
778
|
const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
|
|
745
779
|
const gapSources = new Map();
|
|
746
780
|
const findings = [];
|
|
747
781
|
let missingPages = 0;
|
|
748
782
|
let contradictions = 0;
|
|
749
783
|
for (const page of pages) {
|
|
750
|
-
const resolved = buildResolvedBacklinks(page.id, page.body,
|
|
784
|
+
const resolved = buildResolvedBacklinks(page.id, page.body, wikilinkIndex);
|
|
751
785
|
for (const target of resolved.targets)
|
|
752
786
|
inbound[target]++;
|
|
753
787
|
for (const unresolved of resolved.unresolved) {
|
|
@@ -757,6 +791,11 @@ function runWikiLint(paths, autoFix) {
|
|
|
757
791
|
missingPages++;
|
|
758
792
|
findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
|
|
759
793
|
}
|
|
794
|
+
for (const d of resolved.diagnostics) {
|
|
795
|
+
if (d.code === "link_ambiguous") {
|
|
796
|
+
findings.push(d.message.replace("Ambiguous wikilink: ", "Ambiguous: "));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
760
799
|
}
|
|
761
800
|
let orphans = 0;
|
|
762
801
|
for (const page of pages) {
|
package/dist/mcp/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { join } from "node:path";
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
|
+
import { loadTaskConfig, resolveWikilinkValidation, } from "../extensions/llm-wiki/lib/task-config.js";
|
|
16
17
|
import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
17
18
|
import { createExecApi } from "./exec.js";
|
|
18
19
|
import { bootstrapOperation, captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
|
|
@@ -206,7 +207,7 @@ server.registerTool("wiki_retro", {
|
|
|
206
207
|
};
|
|
207
208
|
}
|
|
208
209
|
const paths = getPaths();
|
|
209
|
-
const result = await retroOperation(paths, slug, title, body, category);
|
|
210
|
+
const result = await retroOperation(paths, slug, title, body, category, resolveWikilinkValidation(loadTaskConfig(process.cwd())));
|
|
210
211
|
if (!result.ok) {
|
|
211
212
|
return {
|
|
212
213
|
content: [
|