@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.de.md +8 -0
  3. package/README.es.md +8 -0
  4. package/README.fr.md +8 -0
  5. package/README.hi.md +8 -0
  6. package/README.ja.md +8 -0
  7. package/README.ko.md +8 -0
  8. package/README.md +88 -2
  9. package/README.pt.md +8 -0
  10. package/README.ru.md +8 -0
  11. package/README.zh.md +8 -0
  12. package/assets/wiki-dashboard.png +0 -0
  13. package/commands/wiki-digest.md +28 -0
  14. package/commands/wiki-discover.md +30 -0
  15. package/commands/wiki-ingest.md +37 -0
  16. package/commands/wiki-init.md +30 -0
  17. package/commands/wiki-lint.md +25 -0
  18. package/commands/wiki-query.md +37 -0
  19. package/commands/wiki-record.md +36 -0
  20. package/commands/wiki-req.md +56 -0
  21. package/commands/wiki-retro.md +35 -0
  22. package/commands/wiki-run.md +31 -0
  23. package/commands/wiki-skills.md +26 -0
  24. package/commands/wiki-status.md +16 -0
  25. package/dist/extensions/llm-wiki/lib/dashboard-command.js +86 -0
  26. package/dist/extensions/llm-wiki/lib/dashboard.js +175 -0
  27. package/dist/extensions/llm-wiki/lib/guardrails.js +30 -1
  28. package/dist/extensions/llm-wiki/lib/host.js +117 -0
  29. package/dist/extensions/llm-wiki/lib/ingest-worker.js +2 -1
  30. package/dist/extensions/llm-wiki/lib/knowledge-document.js +20 -2
  31. package/dist/extensions/llm-wiki/lib/knowledge-links.js +6 -3
  32. package/dist/extensions/llm-wiki/lib/metadata.js +1 -1
  33. package/dist/extensions/llm-wiki/lib/observation.js +31 -3
  34. package/dist/extensions/llm-wiki/lib/settings-command.js +377 -0
  35. package/dist/extensions/llm-wiki/lib/task-config.js +145 -43
  36. package/dist/extensions/llm-wiki/lib/utils.js +59 -16
  37. package/docs/api.md +24 -1
  38. package/docs/commands.md +6 -1
  39. package/docs/configuration.md +62 -11
  40. package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-1-quality-baseline-and-compatibility.md +1520 -0
  41. package/docs/superpowers/roadmaps/2026-08-09-qmd-retrieval-roadmap.md +448 -0
  42. package/docs/superpowers/specs/2026-08-08-qmd-retrieval-design.md +806 -0
  43. package/extensions/llm-wiki/index.ts +48 -6
  44. package/extensions/llm-wiki/lib/dashboard-command.ts +106 -0
  45. package/extensions/llm-wiki/lib/dashboard.ts +210 -0
  46. package/extensions/llm-wiki/lib/guardrails.ts +26 -1
  47. package/extensions/llm-wiki/lib/host.ts +145 -0
  48. package/extensions/llm-wiki/lib/ingest-worker.ts +4 -0
  49. package/extensions/llm-wiki/lib/knowledge-document.ts +20 -2
  50. package/extensions/llm-wiki/lib/knowledge-links.ts +7 -3
  51. package/extensions/llm-wiki/lib/metadata.ts +1 -1
  52. package/extensions/llm-wiki/lib/observation.ts +37 -4
  53. package/extensions/llm-wiki/lib/settings-command.ts +483 -0
  54. package/extensions/llm-wiki/lib/task-config.ts +208 -46
  55. package/extensions/llm-wiki/lib/utils.ts +55 -14
  56. package/package.json +15 -4
  57. package/prompts/wiki-ingest.md +1 -0
  58. package/prompts/wiki-req.md +1 -0
  59. package/prompts/wiki-retro.md +1 -0
  60. package/skills/llm-wiki/SKILL.md +11 -1
@@ -1,6 +1,14 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
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 → { "llm-wiki": { ... } }
16
- * 3. project settings: <cwd>/.pi/settings.json → { "llm-wiki": { ... } }
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 JSON file as a plain object, or `{}` when it is absent or
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 parsed = JSON.parse(readFileSync(path, "utf-8"));
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
- * Persist (or clear) the wiki background `taskModel` in the PROJECT settings
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
- export function persistTaskModel(
262
- cwd: string,
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
- if (model) {
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
- * Persist the agent-trajectory flag in the PROJECT settings file
286
- * `<cwd>/.pi/settings.json` under the namespaced `llm-wiki` key (issue #80).
287
- * Mirrors `persistTaskModel`: project settings win in `loadTaskConfig`, other
288
- * keys are preserved. `true` writes `trajectories: true`; `false` removes the
289
- * key (reverting to the default-off behavior).
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
- export function persistTrajectoriesEnabled(cwd: string, enabled: boolean): void {
292
- const settingsPath = join(cwd, ".pi", "settings.json");
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
- if (enabled) {
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
- let globalPath: string;
313
- try {
314
- globalPath = join(getAgentDir(), "settings.json");
315
- } catch {
316
- globalPath = "";
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
- const projectPath = join(cwd, ".pi", "settings.json");
415
+ return config;
416
+ }
417
+
418
+ // ── Settings source tracking (for /wiki-settings TUI) ──────────
319
419
 
320
- return {
321
- ...TASK_DEFAULTS,
322
- ...(globalPath ? readNamespacedConfig(globalPath) : {}),
323
- ...readNamespacedConfig(projectPath),
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
- return paths.root === getPersonalWikiRoot();
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 from cwd with personal fallback.
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 .llm-wiki/ → project wiki (explicit)
133
- * 2. Walk up from cwd → parent project wiki
134
- * 3. ~/.llm-wiki/ exists → personal wiki
135
- * 4. Fallback: ~/.llm-wiki/ (create personal wiki)
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 resolveVaultRoot(cwd: string): string {
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) !== "none") return 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
- // Check personal wiki at ~/.llm-wiki/
153
- const personalRoot = getPersonalWikiRoot();
154
- if (detectVaultFormat(personalRoot) !== "none") return personalRoot;
182
+ return null;
183
+ }
155
184
 
156
- // Fallback: personal wiki
157
- return personalRoot;
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",
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 .",
@@ -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.
@@ -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]]`.
@@ -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]]`.
@@ -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 13 custom tools (+3 opt-in agent-trajectory tools).
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: