@zosmaai/pi-llm-wiki 0.11.3 → 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/CHANGELOG.md +10 -0
- 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 +88 -2
- 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-digest.md +28 -0
- package/commands/wiki-discover.md +30 -0
- package/commands/wiki-ingest.md +37 -0
- package/commands/wiki-init.md +30 -0
- package/commands/wiki-lint.md +25 -0
- package/commands/wiki-query.md +37 -0
- package/commands/wiki-record.md +36 -0
- package/commands/wiki-req.md +56 -0
- package/commands/wiki-retro.md +35 -0
- package/commands/wiki-run.md +31 -0
- package/commands/wiki-skills.md +26 -0
- package/commands/wiki-status.md +16 -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 +117 -0
- 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 +31 -3
- package/dist/extensions/llm-wiki/lib/settings-command.js +377 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +145 -43
- package/dist/extensions/llm-wiki/lib/utils.js +59 -16
- package/docs/api.md +24 -1
- package/docs/commands.md +6 -1
- package/docs/configuration.md +62 -11
- 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 +48 -6
- 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 +145 -0
- 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 +37 -4
- package/extensions/llm-wiki/lib/settings-command.ts +483 -0
- package/extensions/llm-wiki/lib/task-config.ts +208 -46
- package/extensions/llm-wiki/lib/utils.ts +55 -14
- package/package.json +15 -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
|
@@ -383,6 +383,22 @@ export function serializeKnowledgeDocument(document) {
|
|
|
383
383
|
const body = document.body.replace(/\r\n?/g, "\n").replace(/\n*$/, "");
|
|
384
384
|
return body ? `---\n${yaml}---\n\n${body}\n` : `---\n${yaml}---\n`;
|
|
385
385
|
}
|
|
386
|
+
/** Escape wikilink alias pipes so generated content remains valid in Markdown tables. */
|
|
387
|
+
function escapeWikilinkAliasPipes(body) {
|
|
388
|
+
let inFence = false;
|
|
389
|
+
return body
|
|
390
|
+
.split("\n")
|
|
391
|
+
.map((line) => {
|
|
392
|
+
if (/^\s*(`{3,}|~{3,})/.test(line)) {
|
|
393
|
+
inFence = !inFence;
|
|
394
|
+
return line;
|
|
395
|
+
}
|
|
396
|
+
if (inFence)
|
|
397
|
+
return line;
|
|
398
|
+
return line.replace(/\[\[([^\]\n]*?)(?<!\\)\|([^\]\n]*?)\]\]/g, "[[$1\\|$2]]");
|
|
399
|
+
})
|
|
400
|
+
.join("\n");
|
|
401
|
+
}
|
|
386
402
|
export function createKnowledgeDocument(path, fields, body, sources) {
|
|
387
403
|
if (Object.hasOwn(fields, "sources")) {
|
|
388
404
|
throw new Error("Pass canonical sources as the fourth argument");
|
|
@@ -402,7 +418,7 @@ export function createKnowledgeDocument(path, fields, body, sources) {
|
|
|
402
418
|
const sourcesUnion = sources
|
|
403
419
|
? { kind: "canonical", value: sources }
|
|
404
420
|
: { kind: "absent" };
|
|
405
|
-
const normalizedBody = body.replace(/\r\n?/g, "\n").replace(/\n*$/, "");
|
|
421
|
+
const normalizedBody = escapeWikilinkAliasPipes(body.replace(/\r\n?/g, "\n")).replace(/\n*$/, "");
|
|
406
422
|
return {
|
|
407
423
|
id: path.replace(/\.md$/, ""),
|
|
408
424
|
path,
|
|
@@ -425,7 +441,9 @@ export function patchKnowledgeDocument(document, patch) {
|
|
|
425
441
|
}
|
|
426
442
|
}
|
|
427
443
|
}
|
|
428
|
-
const newBody = patch.body
|
|
444
|
+
const newBody = patch.body
|
|
445
|
+
? escapeWikilinkAliasPipes(patch.body.replace(/\r\n?/g, "\n"))
|
|
446
|
+
: document.body;
|
|
429
447
|
return {
|
|
430
448
|
...document,
|
|
431
449
|
frontmatter: newFrontmatter,
|
|
@@ -3,12 +3,15 @@ import { compareCodePoint } from "./vault-format.js";
|
|
|
3
3
|
function diag(severity, code, path, message) {
|
|
4
4
|
return { severity, code, path, message };
|
|
5
5
|
}
|
|
6
|
+
function normalizeWikilinkTarget(target) {
|
|
7
|
+
return target.trim().replace(/\\$/, "");
|
|
8
|
+
}
|
|
6
9
|
export function extractKnowledgeLinks(body) {
|
|
7
10
|
const markdown = [];
|
|
8
11
|
const wikilinks = [];
|
|
9
|
-
// Extract legacy wikilinks
|
|
12
|
+
// Extract legacy wikilinks. A table-safe alias uses an escaped pipe: [[target\\|alias]].
|
|
10
13
|
for (const match of body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)) {
|
|
11
|
-
wikilinks.push({ target: match[1]
|
|
14
|
+
wikilinks.push({ target: normalizeWikilinkTarget(match[1]), offset: match.index ?? 0 });
|
|
12
15
|
}
|
|
13
16
|
// Parse with CommonMark AST
|
|
14
17
|
let tree;
|
|
@@ -72,7 +75,7 @@ export function extractKnowledgeLinks(body) {
|
|
|
72
75
|
export function extractLegacyWikilinks(body) {
|
|
73
76
|
const links = [];
|
|
74
77
|
for (const match of body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)) {
|
|
75
|
-
links.push({ target: match[1]
|
|
78
|
+
links.push({ target: normalizeWikilinkTarget(match[1]), offset: match.index ?? 0 });
|
|
76
79
|
}
|
|
77
80
|
return links;
|
|
78
81
|
}
|
|
@@ -386,7 +386,7 @@ export function buildDirectoryIndexes(documents, config) {
|
|
|
386
386
|
lines.push("## Directories");
|
|
387
387
|
lines.push("");
|
|
388
388
|
for (const subDir of [...dirs].sort(compareCodePoint)) {
|
|
389
|
-
const encoded = `${
|
|
389
|
+
const encoded = encodeRelativePath(`${subDir}/index.md`);
|
|
390
390
|
lines.push(`- [${escapeLabel(subDir)}/](${encoded})`);
|
|
391
391
|
}
|
|
392
392
|
}
|
|
@@ -239,6 +239,11 @@ export function buildReminderText() {
|
|
|
239
239
|
* `options.display` (issue #77) controls whether the reminder is shown to the
|
|
240
240
|
* user (`true`, the default) or injected silently into model context only
|
|
241
241
|
* (`false`). Pass a resolver so the live `notices` config is read at send time.
|
|
242
|
+
*
|
|
243
|
+
* `options.enabled` gates the reminder entirely — note that `display: false`
|
|
244
|
+
* still injects it into model context, so it is NOT a way to switch the
|
|
245
|
+
* reminder off. Callers pass a resolver that answers "does a wiki apply to the
|
|
246
|
+
* current working directory", evaluated per turn because the session can move.
|
|
242
247
|
*/
|
|
243
248
|
export function registerObservationReminder(pi, reminderState, options) {
|
|
244
249
|
const REMINDER_INTERVAL = options?.turnsBetweenReminders ?? 5;
|
|
@@ -251,9 +256,11 @@ export function registerObservationReminder(pi, reminderState, options) {
|
|
|
251
256
|
return true;
|
|
252
257
|
};
|
|
253
258
|
let turnsSinceLastReminder = 0;
|
|
259
|
+
let agentEndsInUserTurn = 0;
|
|
254
260
|
pi.on("session_start", async () => {
|
|
255
261
|
turnsSinceLastReminder = 0;
|
|
256
262
|
reminderState.observeDoneThisSession = false;
|
|
263
|
+
agentEndsInUserTurn = 0;
|
|
257
264
|
});
|
|
258
265
|
// After compaction, reset turn counter so reminders resume
|
|
259
266
|
// BUT preserve observeDoneThisSession — if the model already called
|
|
@@ -261,17 +268,35 @@ export function registerObservationReminder(pi, reminderState, options) {
|
|
|
261
268
|
pi.on("session_compact", async () => {
|
|
262
269
|
turnsSinceLastReminder = 0;
|
|
263
270
|
});
|
|
271
|
+
// A retry re-runs the agent within the same user turn, and pi does not
|
|
272
|
+
// forward `willRetry` to extensions (zosmaai/pi-llm-wiki#151). Only the
|
|
273
|
+
// user-role message_start separates real user turns from retried agent
|
|
274
|
+
// runs, so the per-turn agent_end count resets there.
|
|
275
|
+
pi.on("message_start", async (event) => {
|
|
276
|
+
if (event.message.role === "user")
|
|
277
|
+
agentEndsInUserTurn = 0;
|
|
278
|
+
});
|
|
264
279
|
pi.on("agent_end", async (event, _ctx) => {
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
280
|
+
// Legacy guard: this pi build does not forward `willRetry` to extension
|
|
281
|
+
// events, but keep the check in case a future one does. The dedup that
|
|
282
|
+
// actually works is the per-turn agent_end count below.
|
|
268
283
|
if ("willRetry" in event && event.willRetry)
|
|
269
284
|
return;
|
|
285
|
+
// No wiki applies here: never nag, and never accumulate a pending reminder
|
|
286
|
+
// that would fire the moment the session moves into a wiki-bearing project.
|
|
287
|
+
if (options?.enabled && !options.enabled())
|
|
288
|
+
return;
|
|
270
289
|
turnsSinceLastReminder++;
|
|
271
290
|
if (turnsSinceLastReminder < REMINDER_INTERVAL)
|
|
272
291
|
return;
|
|
273
292
|
if (reminderState.observeDoneThisSession)
|
|
274
293
|
return;
|
|
294
|
+
// One agent_end per user turn may queue a reminder. A rate-limit storm
|
|
295
|
+
// re-runs the agent several times within the same turn, each firing
|
|
296
|
+
// agent_end, so count them and let only the first through.
|
|
297
|
+
agentEndsInUserTurn++;
|
|
298
|
+
if (agentEndsInUserTurn > 1)
|
|
299
|
+
return;
|
|
275
300
|
pi.sendMessage({
|
|
276
301
|
customType: "wiki-observe-reminder",
|
|
277
302
|
content: buildReminderText(),
|
|
@@ -279,5 +304,8 @@ export function registerObservationReminder(pi, reminderState, options) {
|
|
|
279
304
|
}, {
|
|
280
305
|
deliverAs: "nextTurn",
|
|
281
306
|
});
|
|
307
|
+
// Reset the interval counter after queueing: without it the count stays
|
|
308
|
+
// at/above the threshold and every later agent_end queues a reminder.
|
|
309
|
+
turnsSinceLastReminder = 0;
|
|
282
310
|
});
|
|
283
311
|
}
|
|
@@ -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
|
+
}
|