@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
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname
|
|
3
|
-
import {
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import {
|
|
5
|
+
type HostKind,
|
|
6
|
+
detectHost,
|
|
7
|
+
listGlobalSettingsFiles,
|
|
8
|
+
listProjectSettingsFiles,
|
|
9
|
+
resolveGlobalSettingsPath,
|
|
10
|
+
resolveProjectSettingsPath,
|
|
11
|
+
} from "./host.js";
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* Configuration for the background-task lane (issue #64, part of #63).
|
|
@@ -12,8 +20,11 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
|
12
20
|
*
|
|
13
21
|
* Resolution order (later wins):
|
|
14
22
|
* 1. built-in DEFAULTS
|
|
15
|
-
* 2. global settings: <agentDir>/settings.json
|
|
16
|
-
* 3. project settings: <cwd
|
|
23
|
+
* 2. global settings: <agentDir>/{settings.json,config.yml}
|
|
24
|
+
* 3. project settings: <cwd>/{.pi,.omp}/{settings.json,config.yml}
|
|
25
|
+
*
|
|
26
|
+
* Both host layouts are read (see ./host.ts): pi uses `.pi`, oh-my-pi uses
|
|
27
|
+
* `.omp`, and each file is keyed by the namespaced `llm-wiki` section.
|
|
17
28
|
*
|
|
18
29
|
* When `taskModel` is unset, the background lane falls back to the session
|
|
19
30
|
* model (see Runtime.resolveModel), so the feature is zero-config by default.
|
|
@@ -93,6 +104,27 @@ export interface TaskConfig {
|
|
|
93
104
|
*/
|
|
94
105
|
notices?: boolean;
|
|
95
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Let the PERSONAL wiki act as this project's ambient vault when the project
|
|
109
|
+
* has no wiki of its own.
|
|
110
|
+
*
|
|
111
|
+
* Ambient surfaces are the ones that fire without the user asking: the
|
|
112
|
+
* session notice, the periodic observe/retro reminder, and `before_agent_start`
|
|
113
|
+
* recall injection. `resolveVaultRoot` falls back to the personal vault when
|
|
114
|
+
* a project has none, so with this on those surfaces speak up in EVERY
|
|
115
|
+
* directory once a personal vault exists — injecting reminders and unrelated
|
|
116
|
+
* cross-project recall hits into repositories where no wiki was initialized.
|
|
117
|
+
*
|
|
118
|
+
* Host-dependent default, because the two hosts disagree on what silence
|
|
119
|
+
* means for a globally installed plugin:
|
|
120
|
+
* - pi → `true` (historical behavior, unchanged)
|
|
121
|
+
* - omp → `false` (a repository without a wiki stays quiet)
|
|
122
|
+
*
|
|
123
|
+
* The wiki TOOLS are registered either way, so `/wiki-init` and
|
|
124
|
+
* `wiki_bootstrap` always work; only the unprompted injections are gated.
|
|
125
|
+
*/
|
|
126
|
+
ambientPersonalVault?: boolean;
|
|
127
|
+
|
|
96
128
|
/**
|
|
97
129
|
* Agent-trajectory working-memory (capture → distill → recall), issue #80.
|
|
98
130
|
* OPT-IN, default OFF: only an explicit `trajectories: true` enables it.
|
|
@@ -108,6 +140,13 @@ export interface TaskConfig {
|
|
|
108
140
|
* source content and technical identifiers remain unchanged.
|
|
109
141
|
*/
|
|
110
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;
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
export const TASK_DEFAULTS: TaskConfig = {};
|
|
@@ -120,6 +159,19 @@ export function noticesEnabled(config: TaskConfig | undefined): boolean {
|
|
|
120
159
|
return config?.notices !== false;
|
|
121
160
|
}
|
|
122
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Resolve whether the personal vault may serve as this project's ambient
|
|
164
|
+
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
165
|
+
* (see the field docs on {@link TaskConfig.ambientPersonalVault}).
|
|
166
|
+
*/
|
|
167
|
+
export function personalVaultIsAmbient(
|
|
168
|
+
config: TaskConfig | undefined,
|
|
169
|
+
host: HostKind = detectHost(),
|
|
170
|
+
): boolean {
|
|
171
|
+
if (typeof config?.ambientPersonalVault === "boolean") return config.ambientPersonalVault;
|
|
172
|
+
return host === "pi";
|
|
173
|
+
}
|
|
174
|
+
|
|
123
175
|
/**
|
|
124
176
|
* Resolve whether agent-trajectory working-memory is enabled (issue #80).
|
|
125
177
|
* INVERSE polarity of `noticesEnabled`: defaults to `false`; only an explicit
|
|
@@ -180,6 +232,10 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
180
232
|
out.notices = section.notices;
|
|
181
233
|
}
|
|
182
234
|
|
|
235
|
+
if (typeof section.ambientPersonalVault === "boolean") {
|
|
236
|
+
out.ambientPersonalVault = section.ambientPersonalVault;
|
|
237
|
+
}
|
|
238
|
+
|
|
183
239
|
if (typeof section.trajectories === "boolean") {
|
|
184
240
|
out.trajectories = section.trajectories;
|
|
185
241
|
}
|
|
@@ -190,6 +246,11 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
190
246
|
if (canonical) out.synthesisLanguage = canonical;
|
|
191
247
|
}
|
|
192
248
|
|
|
249
|
+
const maxTokens = section.synthesisMaxTokens;
|
|
250
|
+
if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
251
|
+
out.synthesisMaxTokens = Math.floor(maxTokens);
|
|
252
|
+
}
|
|
253
|
+
|
|
193
254
|
return out;
|
|
194
255
|
} catch {
|
|
195
256
|
return {};
|
|
@@ -235,14 +296,20 @@ export function validateSynthesisLanguage(tag: string): string | undefined {
|
|
|
235
296
|
}
|
|
236
297
|
|
|
237
298
|
/**
|
|
238
|
-
* Read a settings
|
|
299
|
+
* Read a settings file as a plain object, or `{}` when it is absent or
|
|
239
300
|
* corrupt. Reads directly (no `existsSync` pre-check) so there is no
|
|
240
301
|
* check-then-use race: a missing file throws ENOENT, which the catch treats
|
|
241
302
|
* the same as an empty file.
|
|
303
|
+
*
|
|
304
|
+
* `config.yml` / `config.yaml` are parsed as YAML — that is the format oh-my-pi
|
|
305
|
+
* migrates its settings to. Everything else is JSON. JSON is a YAML subset, so
|
|
306
|
+
* the YAML parser also accepts a `.yml` file that actually holds JSON.
|
|
242
307
|
*/
|
|
243
308
|
function readSettingsObject(path: string): Record<string, unknown> {
|
|
244
309
|
try {
|
|
245
|
-
const
|
|
310
|
+
const text = readFileSync(path, "utf-8");
|
|
311
|
+
const parsed =
|
|
312
|
+
path.endsWith(".yml") || path.endsWith(".yaml") ? parseYaml(text) : JSON.parse(text);
|
|
246
313
|
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
247
314
|
} catch {
|
|
248
315
|
// Missing or corrupt settings file: start from an empty object.
|
|
@@ -251,30 +318,17 @@ function readSettingsObject(path: string): Record<string, unknown> {
|
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
/**
|
|
254
|
-
*
|
|
255
|
-
* file `<cwd>/.pi/settings.json` under the namespaced `llm-wiki` key (issue
|
|
256
|
-
* #69). Project settings win over global in `loadTaskConfig`, so this takes
|
|
257
|
-
* effect immediately on the next config load. Other top-level keys and other
|
|
258
|
-
* `llm-wiki` settings are preserved; passing `undefined` removes the key
|
|
259
|
-
* (reverting to the session model).
|
|
321
|
+
* Rewrite the `llm-wiki` section of the global settings file.
|
|
260
322
|
*/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
model: { provider: string; id: string } | undefined,
|
|
264
|
-
): void {
|
|
265
|
-
const settingsPath = join(cwd, ".pi", "settings.json");
|
|
323
|
+
function updateGlobalSection(mutate: (section: Record<string, unknown>) => void): void {
|
|
324
|
+
const settingsPath = resolveGlobalSettingsPath();
|
|
266
325
|
const raw = readSettingsObject(settingsPath);
|
|
267
326
|
|
|
268
327
|
const existing = raw[SETTINGS_KEY];
|
|
269
328
|
const section: Record<string, unknown> =
|
|
270
329
|
existing && typeof existing === "object" ? { ...(existing as Record<string, unknown>) } : {};
|
|
271
330
|
|
|
272
|
-
|
|
273
|
-
section.taskModel = { provider: model.provider, id: model.id };
|
|
274
|
-
} else {
|
|
275
|
-
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
276
|
-
delete section.taskModel;
|
|
277
|
-
}
|
|
331
|
+
mutate(section);
|
|
278
332
|
raw[SETTINGS_KEY] = section;
|
|
279
333
|
|
|
280
334
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
@@ -282,44 +336,152 @@ export function persistTaskModel(
|
|
|
282
336
|
}
|
|
283
337
|
|
|
284
338
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
339
|
+
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
340
|
+
* other top-level key and every other setting in the section.
|
|
341
|
+
*
|
|
342
|
+
* The target file is chosen by `resolveProjectSettingsPath` — `.pi/settings.json`
|
|
343
|
+
* or `.omp/settings.json` depending on host and on what already exists — and is
|
|
344
|
+
* always JSON, which both hosts read.
|
|
290
345
|
*/
|
|
291
|
-
|
|
292
|
-
|
|
346
|
+
function updateProjectSection(
|
|
347
|
+
cwd: string,
|
|
348
|
+
mutate: (section: Record<string, unknown>) => void,
|
|
349
|
+
): void {
|
|
350
|
+
const settingsPath = resolveProjectSettingsPath(cwd);
|
|
293
351
|
const raw = readSettingsObject(settingsPath);
|
|
294
352
|
|
|
295
353
|
const existing = raw[SETTINGS_KEY];
|
|
296
354
|
const section: Record<string, unknown> =
|
|
297
355
|
existing && typeof existing === "object" ? { ...(existing as Record<string, unknown>) } : {};
|
|
298
356
|
|
|
299
|
-
|
|
300
|
-
section.trajectories = true;
|
|
301
|
-
} else {
|
|
302
|
-
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
|
|
303
|
-
delete section.trajectories;
|
|
304
|
-
}
|
|
357
|
+
mutate(section);
|
|
305
358
|
raw[SETTINGS_KEY] = section;
|
|
306
359
|
|
|
307
360
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
308
361
|
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
309
362
|
}
|
|
310
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Persist (or clear) the wiki background `taskModel` in the PROJECT settings
|
|
366
|
+
* file under the namespaced `llm-wiki` key (issue #69). Project settings win
|
|
367
|
+
* over global in `loadTaskConfig`, so this takes effect immediately on the next
|
|
368
|
+
* config load. Passing `undefined` removes the key (reverting to the session
|
|
369
|
+
* model).
|
|
370
|
+
*/
|
|
371
|
+
export function persistTaskModel(
|
|
372
|
+
cwd: string,
|
|
373
|
+
model: { provider: string; id: string } | undefined,
|
|
374
|
+
): void {
|
|
375
|
+
updateProjectSection(cwd, (section) => {
|
|
376
|
+
if (model) {
|
|
377
|
+
section.taskModel = { provider: model.provider, id: model.id };
|
|
378
|
+
} else {
|
|
379
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
380
|
+
delete section.taskModel;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Persist the agent-trajectory flag in the PROJECT settings file under the
|
|
387
|
+
* namespaced `llm-wiki` key (issue #80). Mirrors `persistTaskModel`: `true`
|
|
388
|
+
* writes `trajectories: true`; `false` removes the key (reverting to the
|
|
389
|
+
* default-off behavior).
|
|
390
|
+
*/
|
|
391
|
+
export function persistTrajectoriesEnabled(cwd: string, enabled: boolean): void {
|
|
392
|
+
updateProjectSection(cwd, (section) => {
|
|
393
|
+
if (enabled) {
|
|
394
|
+
section.trajectories = true;
|
|
395
|
+
} else {
|
|
396
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
|
|
397
|
+
delete section.trajectories;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Merge the `llm-wiki` section from every settings file both hosts may use,
|
|
404
|
+
* lowest precedence first: built-in defaults, then user-level files, then
|
|
405
|
+
* project-level files. Absent files contribute nothing.
|
|
406
|
+
*/
|
|
311
407
|
export function loadTaskConfig(cwd: string): TaskConfig {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
408
|
+
const config: TaskConfig = { ...TASK_DEFAULTS };
|
|
409
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
410
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
411
|
+
}
|
|
412
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
413
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
317
414
|
}
|
|
318
|
-
|
|
415
|
+
return config;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Settings source tracking (for /wiki-settings TUI) ──────────
|
|
319
419
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
480
|
};
|
|
481
|
+
|
|
482
|
+
if (scope === "project") {
|
|
483
|
+
updateProjectSection(cwd, mutate);
|
|
484
|
+
} else if (scope === "global") {
|
|
485
|
+
updateGlobalSection(mutate);
|
|
486
|
+
}
|
|
325
487
|
}
|
|
@@ -120,21 +120,45 @@ export function migrateDoubledPersonalVault(
|
|
|
120
120
|
/**
|
|
121
121
|
* Check if a vault is the personal wiki location.
|
|
122
122
|
* Used in layered recall to avoid double-counting.
|
|
123
|
+
*
|
|
124
|
+
* Compares PHYSICAL paths, not strings. On image-based ("atomic") Linux
|
|
125
|
+
* distributions `/home` is a symlink to `var/home`, so `homedir()` yields the
|
|
126
|
+
* `$HOME` string (`/home/u`) while `process.cwd()` — and therefore the root
|
|
127
|
+
* `resolveVaultRoot()` walks up to — yields `/var/home/u`. A string compare
|
|
128
|
+
* calls the personal vault a project vault, which makes layered recall search
|
|
129
|
+
* the same vault twice and `vaultPageCount()` double-count it.
|
|
130
|
+
*
|
|
131
|
+
* Exact equality, NOT containment: a vault nested under the home directory
|
|
132
|
+
* (`~/projects/foo/.llm-wiki`) is a project vault and must stay one.
|
|
123
133
|
*/
|
|
124
134
|
export function isPersonalVault(paths: VaultPaths): boolean {
|
|
125
|
-
|
|
135
|
+
const personalRoot = getPersonalWikiRoot();
|
|
136
|
+
// Fast path: identical strings need no filesystem syscalls.
|
|
137
|
+
if (paths.root === personalRoot) return true;
|
|
138
|
+
try {
|
|
139
|
+
return relativePhysicalPath(personalRoot, paths.root) === "";
|
|
140
|
+
} catch {
|
|
141
|
+
// Unresolvable path (permissions, symlink cycle): fall back to "not
|
|
142
|
+
// personal" so layered recall degrades to searching both vaults rather
|
|
143
|
+
// than silently dropping the personal layer.
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/**
|
|
129
|
-
* Resolve vault root
|
|
149
|
+
* Resolve the vault root that belongs to THIS project, or `null` when the
|
|
150
|
+
* project has none.
|
|
130
151
|
*
|
|
131
152
|
* Priority:
|
|
132
|
-
* 1. cwd has
|
|
133
|
-
* 2.
|
|
134
|
-
* 3.
|
|
135
|
-
*
|
|
153
|
+
* 1. cwd has `.llm-wiki/` (or legacy `.wiki/`) → project wiki (explicit)
|
|
154
|
+
* 2. `WIKI_HOME` → user-selected root, explicit enough to count as the project's
|
|
155
|
+
* 3. Walk up from cwd → parent project wiki (monorepo / nested workspace)
|
|
156
|
+
*
|
|
157
|
+
* Deliberately does NOT fall back to the personal wiki: callers that need the
|
|
158
|
+
* fallback use {@link resolveVaultRoot}, callers that must distinguish "this
|
|
159
|
+
* project has a wiki" from "some wiki exists somewhere" use this.
|
|
136
160
|
*/
|
|
137
|
-
export function
|
|
161
|
+
export function resolveProjectVaultRoot(cwd: string): string | null {
|
|
138
162
|
// A vault rooted at cwd is always the project-local choice.
|
|
139
163
|
if (detectVaultFormat(cwd) !== "none") return cwd;
|
|
140
164
|
|
|
@@ -142,19 +166,36 @@ export function resolveVaultRoot(cwd: string): string {
|
|
|
142
166
|
// over an unrelated personal vault found while walking parent directories.
|
|
143
167
|
if (process.env.WIKI_HOME) return process.env.WIKI_HOME;
|
|
144
168
|
|
|
145
|
-
// Walk up looking for a vault sentinel (new or legacy)
|
|
169
|
+
// Walk up looking for a vault sentinel (new or legacy).
|
|
146
170
|
let dir = cwd;
|
|
147
171
|
while (dir !== dirname(dir)) {
|
|
148
172
|
dir = dirname(dir);
|
|
149
|
-
if (detectVaultFormat(dir)
|
|
173
|
+
if (detectVaultFormat(dir) === "none") continue;
|
|
174
|
+
// Skip the personal vault: it is an ancestor of EVERY project under the
|
|
175
|
+
// home directory (`~/projects/foo`, and on Windows even the temp dir), so
|
|
176
|
+
// counting it here would report a project vault for directories that have
|
|
177
|
+
// none. `resolveVaultRoot` still falls back to it explicitly.
|
|
178
|
+
if (isPersonalVault(getVaultPaths(dir))) continue;
|
|
179
|
+
return dir;
|
|
150
180
|
}
|
|
151
181
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (detectVaultFormat(personalRoot) !== "none") return personalRoot;
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
155
184
|
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Resolve vault root from cwd with personal fallback.
|
|
187
|
+
*
|
|
188
|
+
* Priority:
|
|
189
|
+
* 1-3. {@link resolveProjectVaultRoot}
|
|
190
|
+
* 4. Personal wiki root (`~`, or `WIKI_HOME`) — used whether or not it already
|
|
191
|
+
* holds a vault, so first-run bootstrap has somewhere to write.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveVaultRoot(cwd: string): string {
|
|
194
|
+
// Realpath the personal fallback so a symlinked `$HOME` (atomic-OS layouts)
|
|
195
|
+
// yields the PHYSICAL root the ancestor walk used to return — the #145
|
|
196
|
+
// regression guard pins that. `realpathWithMissingTail` also covers first-run
|
|
197
|
+
// bootstrap, where the personal root does not exist on disk yet.
|
|
198
|
+
return resolveProjectVaultRoot(cwd) ?? realpathWithMissingTail(getPersonalWikiRoot());
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
/** Get all vault paths for the new (.llm-wiki) layout. */
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
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",
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
9
|
"pi-skill",
|
|
10
|
+
"omp",
|
|
11
|
+
"oh-my-pi",
|
|
12
|
+
"omp-plugin",
|
|
13
|
+
"omp-extension",
|
|
10
14
|
"llm-wiki",
|
|
11
15
|
"karpathy",
|
|
12
16
|
"knowledge-base",
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
"extensions",
|
|
38
42
|
"skills",
|
|
39
43
|
"prompts",
|
|
44
|
+
"commands",
|
|
40
45
|
"scripts/migrate-llm-wiki.js",
|
|
41
46
|
"mcp",
|
|
42
47
|
"dist",
|
|
@@ -64,18 +69,24 @@
|
|
|
64
69
|
"llm-wiki": "node ./dist/mcp/index.js"
|
|
65
70
|
}
|
|
66
71
|
},
|
|
72
|
+
"omp": {
|
|
73
|
+
"extensions": [
|
|
74
|
+
"./extensions/llm-wiki/index.ts"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
67
77
|
"peerDependencies": {
|
|
68
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
69
|
-
"typebox": "*"
|
|
78
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
70
79
|
},
|
|
71
80
|
"engines": {
|
|
72
81
|
"node": ">=18"
|
|
73
82
|
},
|
|
74
83
|
"dependencies": {
|
|
75
84
|
"@cfworker/json-schema": "^4.1.1",
|
|
85
|
+
"@mariozechner/pi-tui": "0.70.6",
|
|
76
86
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
77
87
|
"mdast-util-from-markdown": "^2.0.3",
|
|
78
88
|
"node-html-markdown": "^2.0.0",
|
|
89
|
+
"typebox": "^1.1.34",
|
|
79
90
|
"yaml": "^2.9.0",
|
|
80
91
|
"zod": "^4.0"
|
|
81
92
|
},
|
|
@@ -88,7 +99,6 @@
|
|
|
88
99
|
"@types/mdast": "^4.0.4",
|
|
89
100
|
"@types/node": "^22",
|
|
90
101
|
"@vitest/coverage-v8": "^3.2.4",
|
|
91
|
-
"typebox": "^1.1.34",
|
|
92
102
|
"typescript": "^5.7.0",
|
|
93
103
|
"vitest": "^3.0.0"
|
|
94
104
|
},
|
|
@@ -96,6 +106,7 @@
|
|
|
96
106
|
"test": "vitest run",
|
|
97
107
|
"test:watch": "vitest",
|
|
98
108
|
"test:coverage": "vitest run --coverage",
|
|
109
|
+
"build:commands": "node scripts/build-commands.js",
|
|
99
110
|
"build:mcp": "node scripts/build-mcp.js",
|
|
100
111
|
"typecheck": "tsc --noEmit",
|
|
101
112
|
"lint": "biome check .",
|
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:
|