@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,483 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
Container,
|
|
5
|
+
Input,
|
|
6
|
+
type SettingItem,
|
|
7
|
+
SettingsList,
|
|
8
|
+
type SettingsListTheme,
|
|
9
|
+
Spacer,
|
|
10
|
+
Text,
|
|
11
|
+
} from "@mariozechner/pi-tui";
|
|
12
|
+
import type { Runtime } from "./runtime.js";
|
|
13
|
+
import {
|
|
14
|
+
type SettingScope,
|
|
15
|
+
loadTaskConfig,
|
|
16
|
+
loadTaskConfigSources,
|
|
17
|
+
parseModelRef,
|
|
18
|
+
persistSetting,
|
|
19
|
+
trajectoriesEnabled,
|
|
20
|
+
} from "./task-config.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Settings TUI for LLM Wiki.
|
|
24
|
+
*
|
|
25
|
+
* /wiki-settings renders the whole screen as ONE persistent pi-tui
|
|
26
|
+
* SettingsList (the same component pi's own /settings uses), shown through
|
|
27
|
+
* ui.custom():
|
|
28
|
+
*
|
|
29
|
+
* - booleans cycle OFF/ON in place (Enter/Space) — no screen reset,
|
|
30
|
+
* the cursor stays where it is
|
|
31
|
+
* - strings/numbers/model open an inline input submenu; closing it
|
|
32
|
+
* restores the cursor to the same item
|
|
33
|
+
* - every change persists immediately to the chosen scope
|
|
34
|
+
* (project or global) and the display normalizes in place
|
|
35
|
+
*
|
|
36
|
+
* Flow:
|
|
37
|
+
* 1. cwd inside ~/ → global scope; outside ~/ → scope picker first
|
|
38
|
+
* 2. persistent settings screen (Esc closes)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
interface Ui {
|
|
42
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
43
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
44
|
+
/** Hosts that can render a focused custom component (pi ≥ 0.70 interactive). */
|
|
45
|
+
custom?(
|
|
46
|
+
factory: (
|
|
47
|
+
tui: unknown,
|
|
48
|
+
theme: Theme,
|
|
49
|
+
keybindings: unknown,
|
|
50
|
+
done: (result?: unknown) => void,
|
|
51
|
+
) => unknown,
|
|
52
|
+
): Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Setting definition — drives both the menu and its edit menu. */
|
|
56
|
+
interface SettingDef {
|
|
57
|
+
key: string;
|
|
58
|
+
label: string;
|
|
59
|
+
type: "boolean" | "number" | "string" | "model";
|
|
60
|
+
/** Shown with the item when it is selected. */
|
|
61
|
+
hint: string;
|
|
62
|
+
/** Edit-menu title override (defaults to `Set <label>`). */
|
|
63
|
+
menuLabel?: string;
|
|
64
|
+
/** Raw text to prefill the edit input with when the value is unset. */
|
|
65
|
+
defaultText?: string;
|
|
66
|
+
/** Pretty-print a value for display. */
|
|
67
|
+
format: (v: unknown) => string;
|
|
68
|
+
/** Raw text used to prefill the edit input. */
|
|
69
|
+
toEdit: (v: unknown) => string;
|
|
70
|
+
/** Parse user input into a typed value. undefined = invalid. */
|
|
71
|
+
parse: (input: string) => unknown | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SETTINGS: SettingDef[] = [
|
|
75
|
+
{
|
|
76
|
+
key: "taskModel",
|
|
77
|
+
label: "Model",
|
|
78
|
+
type: "model",
|
|
79
|
+
hint: "Model for background wiki tasks (provider/id).",
|
|
80
|
+
menuLabel: "Task model — provider/id, empty = session model",
|
|
81
|
+
format: (v) => {
|
|
82
|
+
const m = v as { provider: string; id: string } | undefined;
|
|
83
|
+
return m ? `${m.provider}/${m.id}` : "(session model)";
|
|
84
|
+
},
|
|
85
|
+
toEdit: (v) => {
|
|
86
|
+
const m = v as { provider: string; id: string } | undefined;
|
|
87
|
+
return m ? `${m.provider}/${m.id}` : "";
|
|
88
|
+
},
|
|
89
|
+
parse: (input) => parseModelRef(input),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "synthesisMaxTokens",
|
|
93
|
+
label: "Synthesis Tokens",
|
|
94
|
+
type: "number",
|
|
95
|
+
hint: "Max output tokens for wiki synthesis runs.",
|
|
96
|
+
defaultText: "16384",
|
|
97
|
+
format: (v) => (v != null ? String(v) : "16384 (default)"),
|
|
98
|
+
toEdit: (v) => (v != null ? String(v) : "16384"),
|
|
99
|
+
parse: (input) => {
|
|
100
|
+
const n = Number(input);
|
|
101
|
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
102
|
+
return Math.floor(n);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "trajectories",
|
|
107
|
+
label: "Trajectories",
|
|
108
|
+
type: "boolean",
|
|
109
|
+
hint: "Capture agent trajectories into the vault.",
|
|
110
|
+
format: (v) => (v ? "ON" : "OFF"),
|
|
111
|
+
toEdit: () => "",
|
|
112
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "notices",
|
|
116
|
+
label: "Notices",
|
|
117
|
+
type: "boolean",
|
|
118
|
+
hint: "Show wiki recall/observation notice lines in chat.",
|
|
119
|
+
format: (v) => (v != null ? (v ? "ON" : "OFF") : "ON (default)"),
|
|
120
|
+
toEdit: () => "",
|
|
121
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "ambientPersonalVault",
|
|
125
|
+
label: "Ambient Personal",
|
|
126
|
+
type: "boolean",
|
|
127
|
+
hint: "Include the personal vault in ambient session context.",
|
|
128
|
+
format: (v) => (v != null ? (v ? "ON" : "OFF") : "host-dependent"),
|
|
129
|
+
toEdit: () => "",
|
|
130
|
+
parse: (input) => Boolean(Boolean(input) && /on|true|1/i.test(input)),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "synthesisLanguage",
|
|
134
|
+
label: "Synthesis Language",
|
|
135
|
+
type: "string",
|
|
136
|
+
hint: "Language for synthesized wiki content.",
|
|
137
|
+
defaultText: "en",
|
|
138
|
+
format: (v) => (v ? String(v) : "en (default)"),
|
|
139
|
+
toEdit: (v) => (v ? String(v) : "en"),
|
|
140
|
+
parse: (input) => {
|
|
141
|
+
const trimmed = input.trim();
|
|
142
|
+
return trimmed || undefined;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: "semanticWeight",
|
|
147
|
+
label: "Semantic Weight",
|
|
148
|
+
type: "number",
|
|
149
|
+
hint: "0–1 weight for embedding (semantic) recall.",
|
|
150
|
+
defaultText: "0.5",
|
|
151
|
+
format: (v) => (v != null ? String(v) : "0.5 (default)"),
|
|
152
|
+
toEdit: (v) => (v != null ? String(v) : "0.5"),
|
|
153
|
+
parse: (input) => {
|
|
154
|
+
const n = Number(input);
|
|
155
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) return undefined;
|
|
156
|
+
return n;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: "recallLinksThreshold",
|
|
161
|
+
label: "Recall Links",
|
|
162
|
+
type: "number",
|
|
163
|
+
hint: "Max recall links shown when the vault is large.",
|
|
164
|
+
defaultText: "50",
|
|
165
|
+
format: (v) => (v != null ? String(v) : "50 (default)"),
|
|
166
|
+
toEdit: (v) => (v != null ? String(v) : "50"),
|
|
167
|
+
parse: (input) => {
|
|
168
|
+
const n = Number(input);
|
|
169
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
170
|
+
return Math.floor(n);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: "recallSkillInlineMax",
|
|
175
|
+
label: "Skill Inline Max",
|
|
176
|
+
type: "number",
|
|
177
|
+
hint: "Max characters inlined from recall into skill prompts.",
|
|
178
|
+
defaultText: "1600",
|
|
179
|
+
format: (v) => (v != null ? String(v) : "1600 (default)"),
|
|
180
|
+
toEdit: (v) => (v != null ? String(v) : "1600"),
|
|
181
|
+
parse: (input) => {
|
|
182
|
+
const n = Number(input);
|
|
183
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
184
|
+
return Math.floor(n);
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: "embeddingProvider",
|
|
189
|
+
label: "Embedding Provider",
|
|
190
|
+
type: "string",
|
|
191
|
+
hint: "Embedding provider (e.g. openai). Empty = embeddings disabled.",
|
|
192
|
+
format: (v) => (v ? String(v) : "(disabled)"),
|
|
193
|
+
toEdit: (v) => (v ? String(v) : ""),
|
|
194
|
+
parse: (input) => input.trim() || undefined,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "embeddingModel",
|
|
198
|
+
label: "Embedding Model",
|
|
199
|
+
type: "string",
|
|
200
|
+
hint: "Embedding model id.",
|
|
201
|
+
defaultText: "text-embedding-3-small",
|
|
202
|
+
format: (v) => (v ? String(v) : "text-embedding-3-small (default)"),
|
|
203
|
+
toEdit: (v) => (v ? String(v) : "text-embedding-3-small"),
|
|
204
|
+
parse: (input) => input.trim() || undefined,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: "embeddingBaseUrl",
|
|
208
|
+
label: "Embedding Base URL",
|
|
209
|
+
type: "string",
|
|
210
|
+
hint: "Custom embeddings API base URL (optional).",
|
|
211
|
+
format: (v) => (v ? String(v) : "—"),
|
|
212
|
+
toEdit: (v) => (v ? String(v) : ""),
|
|
213
|
+
parse: (input) => input.trim() || undefined,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: "embeddingApiKeyEnv",
|
|
217
|
+
label: "Embedding API Key Env",
|
|
218
|
+
type: "string",
|
|
219
|
+
hint: "Environment variable name holding the embeddings API key.",
|
|
220
|
+
defaultText: "OPENAI_API_KEY",
|
|
221
|
+
format: (v) => (v ? String(v) : "OPENAI_API_KEY (default)"),
|
|
222
|
+
toEdit: (v) => (v ? String(v) : "OPENAI_API_KEY"),
|
|
223
|
+
parse: (input) => input.trim() || undefined,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
function isInsideHome(cwd: string): boolean {
|
|
228
|
+
return cwd.startsWith(homedir());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Map effective settings + sources to pi-tui SettingItems.
|
|
233
|
+
*
|
|
234
|
+
* Exported for tests. `notify` is used by edit menus to report invalid input
|
|
235
|
+
* without closing the menu.
|
|
236
|
+
*/
|
|
237
|
+
export function buildSettingItems(
|
|
238
|
+
sources: Record<string, { value: unknown; source: string }>,
|
|
239
|
+
notify: (message: string, type?: "error") => void,
|
|
240
|
+
): SettingItem[] {
|
|
241
|
+
return SETTINGS.map((def) => {
|
|
242
|
+
const entry = sources[def.key];
|
|
243
|
+
const value = entry?.value;
|
|
244
|
+
const source = entry?.source ?? "default";
|
|
245
|
+
|
|
246
|
+
const item: SettingItem = {
|
|
247
|
+
id: def.key,
|
|
248
|
+
label: def.label,
|
|
249
|
+
currentValue: def.format(value),
|
|
250
|
+
description: describeSetting(def, source),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (def.type === "boolean") {
|
|
254
|
+
item.values = ["OFF", "ON"];
|
|
255
|
+
return item;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
item.submenu = (_display, done) => {
|
|
259
|
+
const sub = new InputSubmenu(def.menuLabel ?? `Set ${def.label}`, def.toEdit(value));
|
|
260
|
+
sub.input.onSubmit = (raw) => {
|
|
261
|
+
const trimmed = raw.trim();
|
|
262
|
+
if (def.type === "model") {
|
|
263
|
+
if (!trimmed) {
|
|
264
|
+
done(""); // clear → back to session model
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const ref = parseModelRef(trimmed);
|
|
268
|
+
if (!ref) {
|
|
269
|
+
notify(`LLM Wiki: could not parse "${trimmed}". Use provider/id.`, "error");
|
|
270
|
+
return; // stay in the menu
|
|
271
|
+
}
|
|
272
|
+
done(`${ref.provider}/${ref.id}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!trimmed) {
|
|
276
|
+
done(); // empty = no change
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const parsed = def.parse(trimmed);
|
|
280
|
+
if (parsed === undefined) {
|
|
281
|
+
notify(`LLM Wiki: invalid value "${trimmed}" for ${def.label}`, "error");
|
|
282
|
+
return; // stay in the menu
|
|
283
|
+
}
|
|
284
|
+
done(String(parsed));
|
|
285
|
+
};
|
|
286
|
+
sub.input.onEscape = () => done();
|
|
287
|
+
return sub;
|
|
288
|
+
};
|
|
289
|
+
return item;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function describeSetting(def: SettingDef, source: string): string {
|
|
294
|
+
const where = source === "default" ? "Not set — using default." : `Set in ${source} settings.`;
|
|
295
|
+
return `${def.hint}\n${where}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Parse the display string a SettingItem cycled/edited into back into the
|
|
300
|
+
* typed value to persist.
|
|
301
|
+
*/
|
|
302
|
+
function parseDisplay(def: SettingDef, display: string): unknown {
|
|
303
|
+
if (def.type === "boolean") return display === "ON";
|
|
304
|
+
if (def.type === "model") return display ? parseModelRef(display) : undefined;
|
|
305
|
+
return def.parse(display);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function buildSettingsListTheme(theme: Theme): SettingsListTheme {
|
|
309
|
+
return {
|
|
310
|
+
label: (text, selected) => (selected ? theme.fg("accent", text) : text),
|
|
311
|
+
value: (text, selected) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)),
|
|
312
|
+
description: (text) => theme.fg("dim", text),
|
|
313
|
+
cursor: theme.fg("accent", "→ "),
|
|
314
|
+
hint: (text) => theme.fg("dim", text),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Title + single-line input. SettingsList forwards all key input here. */
|
|
319
|
+
class InputSubmenu extends Container {
|
|
320
|
+
readonly input: Input;
|
|
321
|
+
|
|
322
|
+
constructor(label: string, initialValue: string) {
|
|
323
|
+
super();
|
|
324
|
+
this.addChild(new Text(label, 0, 0));
|
|
325
|
+
this.input = new Input();
|
|
326
|
+
// Type the prefill instead of setValue(): setValue keeps the cursor at 0,
|
|
327
|
+
// which makes backspace no-op and typed input land before the prefill.
|
|
328
|
+
if (initialValue) this.input.handleInput(initialValue);
|
|
329
|
+
this.addChild(this.input);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
handleInput(data: string): void {
|
|
333
|
+
this.input.handleInput(data);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Header + settings list. ui.custom gives this component focus. */
|
|
338
|
+
class SettingsScreen extends Container {
|
|
339
|
+
private list: SettingsList;
|
|
340
|
+
private titleText: Text;
|
|
341
|
+
|
|
342
|
+
constructor(title: string, list: SettingsList) {
|
|
343
|
+
super();
|
|
344
|
+
this.list = list;
|
|
345
|
+
this.titleText = new Text(title, 0, 0);
|
|
346
|
+
this.addChild(this.titleText);
|
|
347
|
+
this.addChild(new Spacer(1));
|
|
348
|
+
this.addChild(list);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
handleInput(data: string): void {
|
|
352
|
+
this.list.handleInput(data);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Update the header line in place (scope changes re-target it live). */
|
|
356
|
+
setTitle(title: string): void {
|
|
357
|
+
this.titleText.setText(title);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Build the header title for the scope currently being written to. */
|
|
362
|
+
function scopeTitle(scope: SettingScope): string {
|
|
363
|
+
return `\u{1F9E0} LLM Wiki Settings \u2014 ${scope === "global" ? "Global" : "Project"} (Esc to close)`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function showSettingsTui(ui: Ui, cwd: string, scope: SettingScope): Promise<void> {
|
|
367
|
+
if (typeof ui.custom !== "function") {
|
|
368
|
+
ui.notify(
|
|
369
|
+
"LLM Wiki: this host does not support the settings screen. Edit settings.json directly.",
|
|
370
|
+
"warning",
|
|
371
|
+
);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The editable Scope row re-targets where subsequent writes land; the
|
|
376
|
+
// picker / inside-home heuristic only chooses the starting value.
|
|
377
|
+
let writeScope = scope;
|
|
378
|
+
const scopeItem: SettingItem = {
|
|
379
|
+
id: "scope",
|
|
380
|
+
label: "Scope",
|
|
381
|
+
currentValue: writeScope === "global" ? "Global" : "Project",
|
|
382
|
+
values: ["Global", "Project"],
|
|
383
|
+
description:
|
|
384
|
+
"Where edits are written. Global \u2192 ~/.pi/agent/settings.json \u00b7 Project \u2192 .pi/settings.json (this folder).",
|
|
385
|
+
};
|
|
386
|
+
const items = [scopeItem, ...buildSettingItems(loadTaskConfigSources(cwd), ui.notify.bind(ui))];
|
|
387
|
+
// Tracks the effective value so the reload note fires on actual changes.
|
|
388
|
+
let prevTrajectories = trajectoriesEnabled(loadTaskConfig(cwd));
|
|
389
|
+
|
|
390
|
+
let list: SettingsList | undefined;
|
|
391
|
+
let screenRef: SettingsScreen | undefined;
|
|
392
|
+
|
|
393
|
+
await ui.custom((_tui, theme, _keybindings, close) => {
|
|
394
|
+
list = new SettingsList(
|
|
395
|
+
items,
|
|
396
|
+
items.length,
|
|
397
|
+
buildSettingsListTheme(theme),
|
|
398
|
+
(id, display) => {
|
|
399
|
+
if (!list) return;
|
|
400
|
+
// Scope row: re-target writes; nothing is persisted for it.
|
|
401
|
+
if (id === "scope") {
|
|
402
|
+
writeScope = display === "Global" ? "global" : "project";
|
|
403
|
+
list.updateValue("scope", display);
|
|
404
|
+
screenRef?.setTitle(scopeTitle(writeScope));
|
|
405
|
+
ui.notify(
|
|
406
|
+
`LLM Wiki: writing to ${
|
|
407
|
+
writeScope === "global"
|
|
408
|
+
? "Global (~/.pi/agent/settings.json)"
|
|
409
|
+
: "Project (.pi/settings.json)"
|
|
410
|
+
}`,
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const def = SETTINGS.find((d) => d.key === id);
|
|
415
|
+
if (!def) return;
|
|
416
|
+
const value = parseDisplay(def, display);
|
|
417
|
+
// Non-model, non-boolean values are validated before done(); undefined
|
|
418
|
+
// here is only expected for the model-clear case.
|
|
419
|
+
if (value === undefined && def.type !== "model") return;
|
|
420
|
+
try {
|
|
421
|
+
persistSetting(cwd, writeScope, def.key, value);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
ui.notify(
|
|
424
|
+
`LLM Wiki: failed to save ${def.label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
425
|
+
"error",
|
|
426
|
+
);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Normalize the displayed value in place (e.g. cleared model shows
|
|
430
|
+
// "(session model)"; "0.50" becomes "0.5").
|
|
431
|
+
list.updateValue(id, def.format(value));
|
|
432
|
+
// trajectories gates tool registration at startup, so a live toggle
|
|
433
|
+
// cannot add/remove the 3 tools mid-session \u2014 say so on real changes.
|
|
434
|
+
if (def.key === "trajectories") {
|
|
435
|
+
const next = Boolean(value);
|
|
436
|
+
if (next !== prevTrajectories) {
|
|
437
|
+
prevTrajectories = next;
|
|
438
|
+
ui.notify(
|
|
439
|
+
"LLM Wiki: trajectory tools register at startup \u2014 new value applies after a reload",
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
() => close(),
|
|
445
|
+
);
|
|
446
|
+
screenRef = new SettingsScreen(scopeTitle(writeScope), list);
|
|
447
|
+
return screenRef;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Register the /wiki-settings command.
|
|
453
|
+
*/
|
|
454
|
+
export function registerWikiSettingsCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
455
|
+
pi.registerCommand("wiki-settings", {
|
|
456
|
+
description: "View and edit LLM Wiki settings (model, tokens, behaviors, embeddings)",
|
|
457
|
+
handler: async (_args: string, ctx: { cwd: string; hasUI: boolean; ui: Ui }) => {
|
|
458
|
+
runtime.ensureConfig(ctx.cwd);
|
|
459
|
+
|
|
460
|
+
if (!ctx.hasUI) {
|
|
461
|
+
ctx.ui.notify(
|
|
462
|
+
"LLM Wiki: /wiki-settings requires an interactive UI. Edit settings.json directly instead.",
|
|
463
|
+
"warning",
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let scope: SettingScope;
|
|
469
|
+
if (isInsideHome(ctx.cwd)) {
|
|
470
|
+
scope = "global";
|
|
471
|
+
} else {
|
|
472
|
+
const pick = await ctx.ui.select("LLM Wiki \u2014 Settings scope?", [
|
|
473
|
+
`Project (${ctx.cwd}/.pi/settings.json)`,
|
|
474
|
+
"Global (~/.pi/agent/settings.json)",
|
|
475
|
+
]);
|
|
476
|
+
if (pick === undefined) return;
|
|
477
|
+
scope = pick.startsWith("Project") ? "project" : "global";
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await showSettingsTui(ctx.ui, ctx.cwd, scope);
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
detectHost,
|
|
7
7
|
listGlobalSettingsFiles,
|
|
8
8
|
listProjectSettingsFiles,
|
|
9
|
+
resolveGlobalSettingsPath,
|
|
9
10
|
resolveProjectSettingsPath,
|
|
10
11
|
} from "./host.js";
|
|
12
|
+
import type { WikilinkValidationMode } from "./knowledge-links.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Configuration for the background-task lane (issue #64, part of #63).
|
|
@@ -139,6 +141,24 @@ export interface TaskConfig {
|
|
|
139
141
|
* source content and technical identifiers remain unchanged.
|
|
140
142
|
*/
|
|
141
143
|
synthesisLanguage?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Max output tokens for the synthesizer sub-agent (issue #160). Default 16384.
|
|
147
|
+
* Reasoning models consume tokens on thinking, so 4096 is too low — the
|
|
148
|
+
* response truncates before commit_synthesis can be called.
|
|
149
|
+
*/
|
|
150
|
+
synthesisMaxTokens?: number;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Wikilink gate applied to the ingested source body before pages are
|
|
154
|
+
* written (issue #172, Layer 2). Reuses the Layer 1 resolver.
|
|
155
|
+
* - "off" : no-op.
|
|
156
|
+
* - "warn" : write proceeds; unresolved/ambiguous links are reported.
|
|
157
|
+
* - "strict" : block the write (commit returns ok:false) if any link is unresolvable.
|
|
158
|
+
* - "normalize" : rewrite resolvable links to their canonical id, then write.
|
|
159
|
+
* Default "warn". See `resolveWikilinkValidation`.
|
|
160
|
+
*/
|
|
161
|
+
wikilinkValidation?: WikilinkValidationMode;
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
export const TASK_DEFAULTS: TaskConfig = {};
|
|
@@ -151,6 +171,24 @@ export function noticesEnabled(config: TaskConfig | undefined): boolean {
|
|
|
151
171
|
return config?.notices !== false;
|
|
152
172
|
}
|
|
153
173
|
|
|
174
|
+
const WIKILINK_VALIDATION_MODES: readonly WikilinkValidationMode[] = [
|
|
175
|
+
"off",
|
|
176
|
+
"warn",
|
|
177
|
+
"strict",
|
|
178
|
+
"normalize",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the wikilink write-gate mode (issue #172, Layer 2). Defaults to
|
|
183
|
+
* `warn` — ingest always writes and reports; only an explicit `strict` blocks.
|
|
184
|
+
* Unknown values fall back to `warn` rather than failing the ingest.
|
|
185
|
+
*/
|
|
186
|
+
export function resolveWikilinkValidation(config: TaskConfig | undefined): WikilinkValidationMode {
|
|
187
|
+
const v = config?.wikilinkValidation;
|
|
188
|
+
if (v && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(v)) return v;
|
|
189
|
+
return "warn";
|
|
190
|
+
}
|
|
191
|
+
|
|
154
192
|
/**
|
|
155
193
|
* Resolve whether the personal vault may serve as this project's ambient
|
|
156
194
|
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
@@ -238,6 +276,16 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
238
276
|
if (canonical) out.synthesisLanguage = canonical;
|
|
239
277
|
}
|
|
240
278
|
|
|
279
|
+
const maxTokens = section.synthesisMaxTokens;
|
|
280
|
+
if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
281
|
+
out.synthesisMaxTokens = Math.floor(maxTokens);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const wl = section.wikilinkValidation;
|
|
285
|
+
if (typeof wl === "string" && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(wl)) {
|
|
286
|
+
out.wikilinkValidation = wl as WikilinkValidationMode;
|
|
287
|
+
}
|
|
288
|
+
|
|
241
289
|
return out;
|
|
242
290
|
} catch {
|
|
243
291
|
return {};
|
|
@@ -304,6 +352,24 @@ function readSettingsObject(path: string): Record<string, unknown> {
|
|
|
304
352
|
return {};
|
|
305
353
|
}
|
|
306
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Rewrite the `llm-wiki` section of the global settings file.
|
|
357
|
+
*/
|
|
358
|
+
function updateGlobalSection(mutate: (section: Record<string, unknown>) => void): void {
|
|
359
|
+
const settingsPath = resolveGlobalSettingsPath();
|
|
360
|
+
const raw = readSettingsObject(settingsPath);
|
|
361
|
+
|
|
362
|
+
const existing = raw[SETTINGS_KEY];
|
|
363
|
+
const section: Record<string, unknown> =
|
|
364
|
+
existing && typeof existing === "object" ? { ...(existing as Record<string, unknown>) } : {};
|
|
365
|
+
|
|
366
|
+
mutate(section);
|
|
367
|
+
raw[SETTINGS_KEY] = section;
|
|
368
|
+
|
|
369
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
370
|
+
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
371
|
+
}
|
|
372
|
+
|
|
307
373
|
/**
|
|
308
374
|
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
309
375
|
* other top-level key and every other setting in the section.
|
|
@@ -383,3 +449,75 @@ export function loadTaskConfig(cwd: string): TaskConfig {
|
|
|
383
449
|
}
|
|
384
450
|
return config;
|
|
385
451
|
}
|
|
452
|
+
|
|
453
|
+
// ── Settings source tracking (for /wiki-settings TUI) ──────────
|
|
454
|
+
|
|
455
|
+
export type SettingScope = "default" | "global" | "project";
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Resolve where each setting is defined: project > global > default.
|
|
459
|
+
*/
|
|
460
|
+
/** All known setting keys — needed because TASK_DEFAULTS is {} (zero-config). */
|
|
461
|
+
const KNOWN_KEYS = [
|
|
462
|
+
"taskModel",
|
|
463
|
+
"embeddingProvider",
|
|
464
|
+
"embeddingModel",
|
|
465
|
+
"embeddingBaseUrl",
|
|
466
|
+
"embeddingApiKey",
|
|
467
|
+
"embeddingApiKeyEnv",
|
|
468
|
+
"semanticWeight",
|
|
469
|
+
"recallLinksThreshold",
|
|
470
|
+
"recallSkillInlineMax",
|
|
471
|
+
"notices",
|
|
472
|
+
"ambientPersonalVault",
|
|
473
|
+
"trajectories",
|
|
474
|
+
"synthesisLanguage",
|
|
475
|
+
"synthesisMaxTokens",
|
|
476
|
+
"wikilinkValidation",
|
|
477
|
+
] as const;
|
|
478
|
+
|
|
479
|
+
export function loadTaskConfigSources(
|
|
480
|
+
cwd: string,
|
|
481
|
+
): Record<string, { value: unknown; source: SettingScope }> {
|
|
482
|
+
const globalResult: Record<string, unknown> = {};
|
|
483
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
484
|
+
Object.assign(globalResult, readNamespacedConfig(path));
|
|
485
|
+
}
|
|
486
|
+
const projectResult: Record<string, unknown> = {};
|
|
487
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
488
|
+
Object.assign(projectResult, readNamespacedConfig(path));
|
|
489
|
+
}
|
|
490
|
+
const effective = loadTaskConfig(cwd);
|
|
491
|
+
|
|
492
|
+
const out: Record<string, { value: unknown; source: SettingScope }> = {};
|
|
493
|
+
for (const key of KNOWN_KEYS) {
|
|
494
|
+
if (key in projectResult) out[key] = { value: projectResult[key], source: "project" };
|
|
495
|
+
else if (key in globalResult) out[key] = { value: globalResult[key], source: "global" };
|
|
496
|
+
else out[key] = { value: (effective as Record<string, unknown>)[key], source: "default" };
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Generic setting persist: writes any single setting to the chosen scope.
|
|
503
|
+
*/
|
|
504
|
+
export function persistSetting(
|
|
505
|
+
cwd: string,
|
|
506
|
+
scope: SettingScope,
|
|
507
|
+
key: string,
|
|
508
|
+
value: unknown,
|
|
509
|
+
): void {
|
|
510
|
+
const mutate = (section: Record<string, unknown>) => {
|
|
511
|
+
if (value === undefined || value === null) {
|
|
512
|
+
delete section[key];
|
|
513
|
+
} else {
|
|
514
|
+
section[key] = value;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (scope === "project") {
|
|
519
|
+
updateProjectSection(cwd, mutate);
|
|
520
|
+
} else if (scope === "global") {
|
|
521
|
+
updateGlobalSection(mutate);
|
|
522
|
+
}
|
|
523
|
+
}
|