@zosmaai/pi-llm-wiki 0.11.4 → 0.11.5
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 +2 -1
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +20 -2
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +6 -3
- package/dist/extensions/llm-wiki/lib/metadata.js +1 -1
- package/dist/extensions/llm-wiki/lib/observation.js +22 -3
- package/dist/extensions/llm-wiki/lib/settings-command.js +377 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +78 -1
- package/docs/api.md +24 -1
- package/docs/commands.md +6 -1
- package/docs/configuration.md +10 -0
- package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-1-quality-baseline-and-compatibility.md +1520 -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 +4 -0
- package/extensions/llm-wiki/lib/knowledge-document.ts +20 -2
- package/extensions/llm-wiki/lib/knowledge-links.ts +7 -3
- package/extensions/llm-wiki/lib/metadata.ts +1 -1
- package/extensions/llm-wiki/lib/observation.ts +23 -3
- package/extensions/llm-wiki/lib/settings-command.ts +483 -0
- package/extensions/llm-wiki/lib/task-config.ts +102 -0
- 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,6 +6,7 @@ import {
|
|
|
6
6
|
detectHost,
|
|
7
7
|
listGlobalSettingsFiles,
|
|
8
8
|
listProjectSettingsFiles,
|
|
9
|
+
resolveGlobalSettingsPath,
|
|
9
10
|
resolveProjectSettingsPath,
|
|
10
11
|
} from "./host.js";
|
|
11
12
|
|
|
@@ -139,6 +140,13 @@ export interface TaskConfig {
|
|
|
139
140
|
* source content and technical identifiers remain unchanged.
|
|
140
141
|
*/
|
|
141
142
|
synthesisLanguage?: string;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Max output tokens for the synthesizer sub-agent (issue #160). Default 16384.
|
|
146
|
+
* Reasoning models consume tokens on thinking, so 4096 is too low — the
|
|
147
|
+
* response truncates before commit_synthesis can be called.
|
|
148
|
+
*/
|
|
149
|
+
synthesisMaxTokens?: number;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
export const TASK_DEFAULTS: TaskConfig = {};
|
|
@@ -238,6 +246,11 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
238
246
|
if (canonical) out.synthesisLanguage = canonical;
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
const maxTokens = section.synthesisMaxTokens;
|
|
250
|
+
if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
251
|
+
out.synthesisMaxTokens = Math.floor(maxTokens);
|
|
252
|
+
}
|
|
253
|
+
|
|
241
254
|
return out;
|
|
242
255
|
} catch {
|
|
243
256
|
return {};
|
|
@@ -304,6 +317,24 @@ function readSettingsObject(path: string): Record<string, unknown> {
|
|
|
304
317
|
return {};
|
|
305
318
|
}
|
|
306
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Rewrite the `llm-wiki` section of the global settings file.
|
|
322
|
+
*/
|
|
323
|
+
function updateGlobalSection(mutate: (section: Record<string, unknown>) => void): void {
|
|
324
|
+
const settingsPath = resolveGlobalSettingsPath();
|
|
325
|
+
const raw = readSettingsObject(settingsPath);
|
|
326
|
+
|
|
327
|
+
const existing = raw[SETTINGS_KEY];
|
|
328
|
+
const section: Record<string, unknown> =
|
|
329
|
+
existing && typeof existing === "object" ? { ...(existing as Record<string, unknown>) } : {};
|
|
330
|
+
|
|
331
|
+
mutate(section);
|
|
332
|
+
raw[SETTINGS_KEY] = section;
|
|
333
|
+
|
|
334
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
335
|
+
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
336
|
+
}
|
|
337
|
+
|
|
307
338
|
/**
|
|
308
339
|
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
309
340
|
* other top-level key and every other setting in the section.
|
|
@@ -383,3 +414,74 @@ export function loadTaskConfig(cwd: string): TaskConfig {
|
|
|
383
414
|
}
|
|
384
415
|
return config;
|
|
385
416
|
}
|
|
417
|
+
|
|
418
|
+
// ── Settings source tracking (for /wiki-settings TUI) ──────────
|
|
419
|
+
|
|
420
|
+
export type SettingScope = "default" | "global" | "project";
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Resolve where each setting is defined: project > global > default.
|
|
424
|
+
*/
|
|
425
|
+
/** All known setting keys — needed because TASK_DEFAULTS is {} (zero-config). */
|
|
426
|
+
const KNOWN_KEYS = [
|
|
427
|
+
"taskModel",
|
|
428
|
+
"embeddingProvider",
|
|
429
|
+
"embeddingModel",
|
|
430
|
+
"embeddingBaseUrl",
|
|
431
|
+
"embeddingApiKey",
|
|
432
|
+
"embeddingApiKeyEnv",
|
|
433
|
+
"semanticWeight",
|
|
434
|
+
"recallLinksThreshold",
|
|
435
|
+
"recallSkillInlineMax",
|
|
436
|
+
"notices",
|
|
437
|
+
"ambientPersonalVault",
|
|
438
|
+
"trajectories",
|
|
439
|
+
"synthesisLanguage",
|
|
440
|
+
"synthesisMaxTokens",
|
|
441
|
+
] as const;
|
|
442
|
+
|
|
443
|
+
export function loadTaskConfigSources(
|
|
444
|
+
cwd: string,
|
|
445
|
+
): Record<string, { value: unknown; source: SettingScope }> {
|
|
446
|
+
const globalResult: Record<string, unknown> = {};
|
|
447
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
448
|
+
Object.assign(globalResult, readNamespacedConfig(path));
|
|
449
|
+
}
|
|
450
|
+
const projectResult: Record<string, unknown> = {};
|
|
451
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
452
|
+
Object.assign(projectResult, readNamespacedConfig(path));
|
|
453
|
+
}
|
|
454
|
+
const effective = loadTaskConfig(cwd);
|
|
455
|
+
|
|
456
|
+
const out: Record<string, { value: unknown; source: SettingScope }> = {};
|
|
457
|
+
for (const key of KNOWN_KEYS) {
|
|
458
|
+
if (key in projectResult) out[key] = { value: projectResult[key], source: "project" };
|
|
459
|
+
else if (key in globalResult) out[key] = { value: globalResult[key], source: "global" };
|
|
460
|
+
else out[key] = { value: (effective as Record<string, unknown>)[key], source: "default" };
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Generic setting persist: writes any single setting to the chosen scope.
|
|
467
|
+
*/
|
|
468
|
+
export function persistSetting(
|
|
469
|
+
cwd: string,
|
|
470
|
+
scope: SettingScope,
|
|
471
|
+
key: string,
|
|
472
|
+
value: unknown,
|
|
473
|
+
): void {
|
|
474
|
+
const mutate = (section: Record<string, unknown>) => {
|
|
475
|
+
if (value === undefined || value === null) {
|
|
476
|
+
delete section[key];
|
|
477
|
+
} else {
|
|
478
|
+
section[key] = value;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (scope === "project") {
|
|
483
|
+
updateProjectSection(cwd, mutate);
|
|
484
|
+
} else if (scope === "global") {
|
|
485
|
+
updateGlobalSection(mutate);
|
|
486
|
+
}
|
|
487
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -75,17 +75,18 @@
|
|
|
75
75
|
]
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
79
|
-
"typebox": "*"
|
|
78
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
80
79
|
},
|
|
81
80
|
"engines": {
|
|
82
81
|
"node": ">=18"
|
|
83
82
|
},
|
|
84
83
|
"dependencies": {
|
|
85
84
|
"@cfworker/json-schema": "^4.1.1",
|
|
85
|
+
"@mariozechner/pi-tui": "0.70.6",
|
|
86
86
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
87
87
|
"mdast-util-from-markdown": "^2.0.3",
|
|
88
88
|
"node-html-markdown": "^2.0.0",
|
|
89
|
+
"typebox": "^1.1.34",
|
|
89
90
|
"yaml": "^2.9.0",
|
|
90
91
|
"zod": "^4.0"
|
|
91
92
|
},
|
|
@@ -98,7 +99,6 @@
|
|
|
98
99
|
"@types/mdast": "^4.0.4",
|
|
99
100
|
"@types/node": "^22",
|
|
100
101
|
"@vitest/coverage-v8": "^3.2.4",
|
|
101
|
-
"typebox": "^1.1.34",
|
|
102
102
|
"typescript": "^5.7.0",
|
|
103
103
|
"vitest": "^3.0.0"
|
|
104
104
|
},
|
package/prompts/wiki-ingest.md
CHANGED
|
@@ -33,4 +33,5 @@ $ARGUMENTS
|
|
|
33
33
|
**Rules:**
|
|
34
34
|
- Never modify files in `raw/` — source packets are immutable after capture.
|
|
35
35
|
- Never fabricate information — always cite sources with `[[sources/SRC-...]]`.
|
|
36
|
+
- Inside Markdown table cells, write aliased wikilinks as `[[target\|alias]]`, never `[[target|alias]]`.
|
|
36
37
|
- The extension auto-updates metadata — you do NOT need to manually edit `meta/` files.
|
package/prompts/wiki-req.md
CHANGED
|
@@ -53,3 +53,4 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
53
53
|
- Use status values: `draft` → `clarified` → `active` → `implemented` → `deferred` → `rejected`
|
|
54
54
|
- Use priority values: `p0` (blocking), `p1` (critical), `p2` (important), `p3` (nice-to-have)
|
|
55
55
|
- Do not create requirements in `raw/` — that layer is for external source artifacts only
|
|
56
|
+
- Inside Markdown table cells, write aliased wikilinks as `[[target\|alias]]`, never `[[target|alias]]`.
|
package/prompts/wiki-retro.md
CHANGED
|
@@ -32,3 +32,4 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
32
32
|
- One atomic insight per `wiki_retro` call. Use multiple calls for multiple insights.
|
|
33
33
|
- Don't save obvious things. Save non-obvious patterns, tradeoffs, and design decisions.
|
|
34
34
|
- Always add `[[wikilinks]]` to connect the new insight with existing wiki knowledge.
|
|
35
|
+
- Inside Markdown table cells, write aliased wikilinks as `[[target\|alias]]`, never `[[target|alias]]`.
|
package/skills/llm-wiki/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: llm-wiki
|
|
3
|
-
description: Build and maintain a persistent, interlinked Obsidian-compatible markdown wiki using Karpathy's LLM Wiki pattern. Extension-backed with auto-generated metadata, guardrails, and
|
|
3
|
+
description: Build and maintain a persistent, interlinked Obsidian-compatible markdown wiki using Karpathy's LLM Wiki pattern. Extension-backed with auto-generated metadata, guardrails, and 14 custom tools (+3 opt-in agent-trajectory tools).
|
|
4
4
|
whenToUse: Call wiki_recall at task start to find relevant wiki pages. Call wiki_retro at task end to save new insights. When agent-trajectory working-memory is enabled (opt-in, /wiki-trajectories on), also call wiki_recall_skill at task start to find reusable skills / past cases ("have I done this before?") and wiki_capture_trajectory after non-trivial tasks to record how you solved them. The extension injects a brief status line, but explicit calls with task-specific terms get better results.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -178,6 +178,16 @@ The choice is persisted to project settings (`.pi/settings.json` under `llm-wiki
|
|
|
178
178
|
wiki_ingest(model="anthropic/claude-haiku")
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
### Settings Screen (`/wiki-settings`)
|
|
182
|
+
|
|
183
|
+
**Interactive settings:** run `/wiki-settings` to open a persistent settings screen. It lists every
|
|
184
|
+
`llm-wiki` setting with its current value and where it is set (project overrides global; the
|
|
185
|
+
default is shown when unset). Booleans cycle in place with Enter/Space; numbers, strings, and the
|
|
186
|
+
model edit inline in a prefilled input. Every change persists immediately to the chosen scope
|
|
187
|
+
(project or global). Setting the model to `session` clears `llm-wiki.taskModel` (back to the session model).
|
|
188
|
+
|
|
189
|
+
### Dashboard Screen (`/wiki-dashboard`) **Read-only vault health:** run `/wiki-dashboard` to open a persistent read-only screen: page counts by type + total size, last page touch + stale (30d+) count, activity by kind last 7 days (observes/retros/syntheses), pending raw-source ingest queue, zero-backlink page count (see `/wiki-lint` full scan), embedding coverage (emb/ files vs. pages). All values computed from existing on-disk state — no writes, no LLM calls. Esc closes.
|
|
190
|
+
|
|
181
191
|
### Auto-Bootstrap (One-Time)
|
|
182
192
|
|
|
183
193
|
The extension creates the wiki vault automatically on startup. On the first turn, it injects a directive asking you to infer topic and mode, then call:
|