@zosmaai/pi-llm-wiki 0.11.7 → 0.12.0

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 (55) hide show
  1. package/README.md +7 -0
  2. package/dist/extensions/llm-wiki/lib/dashboard-command.js +7 -2
  3. package/dist/extensions/llm-wiki/lib/dashboard.js +1 -1
  4. package/dist/extensions/llm-wiki/lib/guardrails.js +1 -1
  5. package/dist/extensions/llm-wiki/lib/host.js +2 -2
  6. package/dist/extensions/llm-wiki/lib/ingest-worker.js +1 -1
  7. package/dist/extensions/llm-wiki/lib/knowledge-links.js +1 -1
  8. package/dist/extensions/llm-wiki/lib/legacy-repair.js +1 -1
  9. package/dist/extensions/llm-wiki/lib/metadata.js +1 -1
  10. package/dist/extensions/llm-wiki/lib/model-command.js +77 -4
  11. package/dist/extensions/llm-wiki/lib/recall.js +2 -2
  12. package/dist/extensions/llm-wiki/lib/runtime.js +1 -1
  13. package/dist/extensions/llm-wiki/lib/settings-command.js +7 -2
  14. package/dist/extensions/llm-wiki/lib/source-extractors.js +21 -5
  15. package/dist/extensions/llm-wiki/lib/source-packet.js +10 -2
  16. package/dist/extensions/llm-wiki/lib/subagent.js +1 -1
  17. package/dist/extensions/llm-wiki/lib/tools.js +4 -1
  18. package/dist/extensions/llm-wiki/lib/trajectory.js +1 -1
  19. package/dist/extensions/llm-wiki/lib/utils.js +1 -1
  20. package/dist/extensions/llm-wiki/lib/vault-format.js +1 -1
  21. package/dist/extensions/llm-wiki/lib/visible-status.js +1 -1
  22. package/dist/mcp/operations.js +1 -1
  23. package/docs/superpowers/plans/2026-08-29-custom-wiki-page-types.md +378 -0
  24. package/docs/superpowers/plans/2026-08-29-keyless-provider-ingest-fix.md +256 -0
  25. package/docs/superpowers/plans/2026-09-02-wiki-lint-fresh-vault-and-windows-capture-fixes.md +284 -0
  26. package/extensions/llm-wiki/index.ts +1 -1
  27. package/extensions/llm-wiki/lib/bootstrap.ts +2 -2
  28. package/extensions/llm-wiki/lib/dashboard-command.ts +10 -4
  29. package/extensions/llm-wiki/lib/dashboard.ts +1 -1
  30. package/extensions/llm-wiki/lib/embeddings.ts +1 -1
  31. package/extensions/llm-wiki/lib/guardrails.ts +3 -3
  32. package/extensions/llm-wiki/lib/host.ts +4 -4
  33. package/extensions/llm-wiki/lib/ingest-worker.ts +8 -8
  34. package/extensions/llm-wiki/lib/knowledge-links.ts +1 -1
  35. package/extensions/llm-wiki/lib/legacy-repair.ts +4 -4
  36. package/extensions/llm-wiki/lib/metadata.ts +3 -3
  37. package/extensions/llm-wiki/lib/model-command.ts +116 -6
  38. package/extensions/llm-wiki/lib/observation.ts +2 -2
  39. package/extensions/llm-wiki/lib/recall.ts +5 -5
  40. package/extensions/llm-wiki/lib/retro.ts +2 -2
  41. package/extensions/llm-wiki/lib/runtime.ts +2 -2
  42. package/extensions/llm-wiki/lib/settings-command.ts +11 -4
  43. package/extensions/llm-wiki/lib/source-extractors.ts +20 -12
  44. package/extensions/llm-wiki/lib/source-packet.ts +13 -5
  45. package/extensions/llm-wiki/lib/subagent.ts +2 -2
  46. package/extensions/llm-wiki/lib/task-config.ts +1 -1
  47. package/extensions/llm-wiki/lib/tools.ts +7 -4
  48. package/extensions/llm-wiki/lib/trajectories-command.ts +1 -1
  49. package/extensions/llm-wiki/lib/trajectory.ts +3 -3
  50. package/extensions/llm-wiki/lib/utils.ts +2 -2
  51. package/extensions/llm-wiki/lib/vault-format.ts +2 -2
  52. package/extensions/llm-wiki/lib/visible-status.ts +1 -1
  53. package/extensions/llm-wiki/lib/wiki-service.ts +1 -1
  54. package/mcp/operations.ts +4 -4
  55. package/package.json +9 -9
@@ -0,0 +1,284 @@
1
+ # Wiki Lint Fresh-Vault + Windows Capture Fixes Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use /skill:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Fix two one-line bugs — `wiki_lint` crashing with ENOENT on fresh checkouts (issue #203) and `wiki_capture_source` corrupting the `original/` artifact name on Windows absolute paths (issue #204) — each with a regression test.
6
+
7
+ **Architecture:** Both fixes slot into existing code with no new modules. #203: `runWikiLint` (private in `tools.ts`) persists the gap snapshot via `writeJson` without ensuring `.llm-wiki/.discoveries/` exists; it gets the same `mkdirSync(recursive)` treatment the `autoFix` report write in the same function already uses. #204: `fileCaptureSource` derives the artifact name with `filePath.split("/").pop()`, which is a no-op on Windows paths; replace with a small exported, platform-aware `originalFileNameForPath()` helper modeled on the existing `originalFileNameForUrl()` sibling so the behavior is unit-testable cross-platform.
8
+
9
+ **Tech Stack:** TypeScript (ES2022, ESM), Vitest, Biome, pnpm.
10
+
11
+ **Roadmap:** None
12
+
13
+ **Phase:** Single-plan implementation (two independent one-line fixes, one branch, one PR)
14
+
15
+ ---
16
+
17
+ ## Background (what the engineer must know)
18
+
19
+ - Repo: `@zosmaai/pi-llm-wiki`. Two bugs, both confirmed present on current `main`:
20
+ - **#203** — `runWikiLint()` (`extensions/llm-wiki/lib/tools.ts:879`) unconditionally writes `gaps.json` to `paths.discoveries` at ~line 1018. `ensureVaultStructure` (`lib/utils.ts:262`) does create `.discoveries` — but only when bootstrapping a vault. A fresh git checkout/worktree of an existing vault has `.discoveries` gitignored → absent → `writeJson` → `writeFileSync` throws ENOENT → the whole lint run is discarded and no report is ever delivered.
21
+ - **#204** — `fileCaptureSource()` (`extensions/llm-wiki/lib/source-packet.ts:114`) computes `const fileName = filePath.split("/").pop() || "unknown"`. On Windows an absolute path like `D:\any\dir\doc.md` contains no `/`, so `fileName` becomes the whole string; `preserveFileOriginal` then writes `join(packetPath, "original", "D:\any\dir\doc.md")` — a drive colon inside a filename segment, illegal on Windows → capture fails, empty packet left behind.
22
+ - Test harness facts (verified, so the tests below compile as-is):
23
+ - `test/lint-okf.test.ts` drives the tool via `registerWikiLint({ registerTool: ... } as unknown as ExtensionAPI)` and `tool.execute("test", params, undefined, undefined, { cwd: root, hasUI: false })`. It imports `existsSync`, `rmSync`, `writeFileSync`, `readFileSync` from `node:fs` and `getVaultPaths`, `ensureVaultStructure` from `lib/utils.js` — everything this plan's test needs is already imported.
24
+ - Without a `Runtime` (the test case), `dispatchReported` (`tools.ts:82`) runs `work()` inline with **no catch** — so the ENOENT rejection propagates out of `tool.execute()`. That is why the failing test below fails by throwing.
25
+ - `test/source-capture.test.ts` already imports `{ captureFile, captureText, captureUrl }` from `lib/source-packet.js` and has a `makePaths()` helper + `mockPi()` from `./helpers.js`.
26
+ - Commands: `pnpm test` (vitest), `pnpm typecheck`, `pnpm lint` (biome). Node/pnpm available; `pnpm --version` = 9.15.4.
27
+ - Issue #171 is explicitly out of scope.
28
+
29
+ ## File Structure
30
+
31
+ | File | Change | Responsibility in this plan |
32
+ |---|---|---|
33
+ | `extensions/llm-wiki/lib/tools.ts` | Modify (~line 1015, inside `runWikiLint`) | One-line `mkdirSync(paths.discoveries, { recursive: true })` before the gaps write |
34
+ | `extensions/llm-wiki/lib/source-packet.ts` | Modify (line 3 import, line 114, new helper after `originalFileNameForUrl` at ~line 256) | Export `originalFileNameForPath(filePath)`; use it in `fileCaptureSource` |
35
+ | `test/lint-okf.test.ts` | Modify (append one `it(...)` after the "refreshes a stale gap snapshot to empty on audit-only lint" test, ~line 183) | Regression test: lint on a vault without `.discoveries` completes and persists `gaps.json` |
36
+ | `test/source-capture.test.ts` | Modify (extend the `source-packet.js` import; add one `it(...)` after the "should copy local non-PDF file content to extracted.md" test) | Unit test: `originalFileNameForPath` strips the platform's directory prefix |
37
+
38
+ No new files. No config, docs, or schema changes.
39
+
40
+ ---
41
+
42
+ ### Task 1: Fix #203 — `wiki_lint` ENOENT on fresh vault
43
+
44
+ **Files:**
45
+ - Modify: `extensions/llm-wiki/lib/tools.ts` (inside `runWikiLint`, the gaps write at ~lines 1015-1020)
46
+ - Test: `test/lint-okf.test.ts`
47
+
48
+ - [x] **Step 1: Write the failing test**
49
+
50
+ Append this `it(...)` to `test/lint-okf.test.ts`, immediately after the closing `});` of the test named `"refreshes a stale gap snapshot to empty on audit-only lint"` (that test ends with `expect(status.content[0].text).toContain("Gaps: 0");` + `});`, ~line 183). All symbols used are already imported in this file:
51
+
52
+ ```ts
53
+ it("completes on a fresh checkout where .discoveries is not yet created (issue #203)", async () => {
54
+ const paths = getVaultPaths(root);
55
+ ensureVaultStructure(paths);
56
+ // Fresh checkouts / worktrees of an existing vault do not have the
57
+ // gitignored .discoveries directory — simulate that.
58
+ rmSync(paths.discoveries, { recursive: true, force: true });
59
+ writeFileSync(join(paths.dotWiki, "config.json"), JSON.stringify({ name: "Fresh lint" }));
60
+ writeFileSync(
61
+ join(paths.wiki, "concepts", "valid.md"),
62
+ "---\ntype: concept\n---\n\nValid.\n",
63
+ );
64
+
65
+ let tool: TestTool | undefined;
66
+ registerWikiLint({
67
+ registerTool: (definition: unknown) => {
68
+ tool = definition as TestTool;
69
+ },
70
+ } as unknown as ExtensionAPI);
71
+ if (!tool) throw new Error("wiki_lint was not registered");
72
+ const result = await tool.execute("test", { auto_fix: false }, undefined, undefined, {
73
+ cwd: root,
74
+ hasUI: false,
75
+ });
76
+
77
+ expect(result.isError).not.toBe(true);
78
+ // auto_fix:false returns the run summary (not the reportLines file content).
79
+ expect(result.content[0].text).toContain("LLM Wiki lint complete");
80
+ expect(existsSync(join(paths.discoveries, "gaps.json"))).toBe(true);
81
+ const snapshot = JSON.parse(readFileSync(join(paths.discoveries, "gaps.json"), "utf8"));
82
+ expect(snapshot.gaps).toEqual([]);
83
+ });
84
+ ```
85
+
86
+ - [x] **Step 2: Run the test to verify it fails**
87
+
88
+ Run: `pnpm vitest run test/lint-okf.test.ts -t "fresh checkout"`
89
+
90
+ Expected: FAIL — the `await tool.execute(...)` line throws `Error: ENOENT: no such file or directory, open '<root>/.llm-wiki/.discoveries/gaps.json'` (rejection propagates because `dispatchReported` has no runtime in tests).
91
+
92
+ - [x] **Step 3: Write the minimal implementation**
93
+
94
+ In `extensions/llm-wiki/lib/tools.ts`, inside `runWikiLint`, change this block (~lines 1015-1020):
95
+
96
+ ```ts
97
+ // The gap snapshot is generated discovery metadata consumed by wiki_status:
98
+ // persist it on every successful lint so status never reports a stale count.
99
+ // Corrective actions below (report, event, meta rebuild) stay autoFix-only.
100
+ writeJson(join(paths.discoveries, "gaps.json"), {
101
+ gaps,
102
+ generated: new Date().toISOString(),
103
+ });
104
+ ```
105
+
106
+ to:
107
+
108
+ ```ts
109
+ // The gap snapshot is generated discovery metadata consumed by wiki_status:
110
+ // persist it on every successful lint so status never reports a stale count.
111
+ // Corrective actions below (report, event, meta rebuild) stay autoFix-only.
112
+ // mkdir mirrors the autoFix report write below: on a fresh checkout the
113
+ // gitignored .discoveries dir does not exist yet (issue #203).
114
+ mkdirSync(paths.discoveries, { recursive: true });
115
+ writeJson(join(paths.discoveries, "gaps.json"), {
116
+ gaps,
117
+ generated: new Date().toISOString(),
118
+ });
119
+ ```
120
+
121
+ `mkdirSync` is already imported at the top of `tools.ts` (line 1: `import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";`) — no import change.
122
+
123
+ - [x] **Step 4: Run the test to verify it passes**
124
+
125
+ Run: `pnpm vitest run test/lint-okf.test.ts`
126
+
127
+ Expected: PASS — all tests in the file green, including the new one.
128
+
129
+ - [x] **Step 5: Run the full gate**
130
+
131
+ Run: `pnpm test && pnpm typecheck && pnpm lint`
132
+
133
+ Expected: all green (no unrelated breakage).
134
+
135
+ - [x] **Step 6: Commit**
136
+
137
+ ```bash
138
+ git add test/lint-okf.test.ts extensions/llm-wiki/lib/tools.ts
139
+ git commit -m "fix: ensure .discoveries exists before lint persists the gap snapshot (#203)"
140
+ ```
141
+
142
+ ---
143
+
144
+ ### Task 2: Fix #204 — Windows drive colon in captured file name
145
+
146
+ **Files:**
147
+ - Modify: `extensions/llm-wiki/lib/source-packet.ts` (line 3 import; line 114 in `fileCaptureSource`; new exported helper after `originalFileNameForUrl`, which ends ~line 256)
148
+ - Test: `test/source-capture.test.ts`
149
+
150
+ - [x] **Step 1: Write the failing test**
151
+
152
+ First, extend the existing import at line 5 of `test/source-capture.test.ts`:
153
+
154
+ ```ts
155
+ import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
156
+ ```
157
+
158
+ becomes:
159
+
160
+ ```ts
161
+ import {
162
+ captureFile,
163
+ captureText,
164
+ captureUrl,
165
+ originalFileNameForPath,
166
+ } from "../extensions/llm-wiki/lib/source-packet.js";
167
+ ```
168
+
169
+ Then append this `it(...)` inside the `describe("source packet capture", ...)` block, immediately after the test named `"should copy local non-PDF file content to extracted.md"` (ends with `expect(existsSync(join(result.packetPath, "original", "notes.md"))).toBe(true);` + `});`):
170
+
171
+ ```ts
172
+ it("derives the original artifact name with platform basename so drive letters stay out of file names (issue #204)", () => {
173
+ const absPath =
174
+ process.platform === "win32" ? "D:\\any\\dir\\doc.md" : "/home/user/any/dir/doc.md";
175
+ expect(originalFileNameForPath(absPath)).toBe("doc.md");
176
+ });
177
+ ```
178
+
179
+ - [x] **Step 2: Run the test to verify it fails**
180
+
181
+ Run: `pnpm vitest run test/source-capture.test.ts -t "drive letters"`
182
+
183
+ Expected: FAIL — module load error: `No export named "originalFileNameForPath"` (the named ESM import does not exist yet).
184
+
185
+ - [x] **Step 3: Write the minimal implementation**
186
+
187
+ In `extensions/llm-wiki/lib/source-packet.ts`, make three edits:
188
+
189
+ 1. Line 3 import — change:
190
+
191
+ ```ts
192
+ import { extname, join } from "node:path";
193
+ ```
194
+
195
+ to:
196
+
197
+ ```ts
198
+ import { basename, extname, join } from "node:path";
199
+ ```
200
+
201
+ 2. After `originalFileNameForUrl` (the function ending `return "source.html";` + `});`/`}`, ~line 256), add:
202
+
203
+ ```ts
204
+ /**
205
+ * Original-artifact name for a local file capture. Uses platform basename so
206
+ * Windows absolute paths (drive colon + backslashes) cannot leak into the
207
+ * packet/original file name (issue #204).
208
+ */
209
+ export function originalFileNameForPath(filePath: string): string {
210
+ return basename(filePath) || "unknown";
211
+ }
212
+ ```
213
+
214
+ 3. Inside `fileCaptureSource` (~line 114) — change:
215
+
216
+ ```ts
217
+ const fileName = filePath.split("/").pop() || "unknown";
218
+ ```
219
+
220
+ to:
221
+
222
+ ```ts
223
+ const fileName = originalFileNameForPath(filePath);
224
+ ```
225
+
226
+ `fileName` flows into `preserveFileOriginal(packetPath, filePath, fileName, content)` and the manifest `title` — both now get the clean basename.
227
+
228
+ - [x] **Step 4: Run the test to verify it passes**
229
+
230
+ Run: `pnpm vitest run test/source-capture.test.ts`
231
+
232
+ Expected: PASS — all tests in the file green, including the new one.
233
+
234
+ Note (honest ceiling): on Linux, POSIX `basename("D:\\any\\dir\\doc.md")` returns the string unchanged, so this assertion only has teeth on `win32` — it pins the operation choice (platform basename vs. slash-split) rather than reproducing Windows. Full Windows coverage needs a Windows CI leg or manual repro.
235
+
236
+ - [x] **Step 5: Run the full gate**
237
+
238
+ Run: `pnpm test && pnpm typecheck && pnpm lint`
239
+
240
+ Expected: all green.
241
+
242
+ - [x] **Step 6: Commit**
243
+
244
+ ```bash
245
+ git add test/source-capture.test.ts extensions/llm-wiki/lib/source-packet.ts
246
+ git commit -m "fix: use platform basename for file capture artifact names (#204)"
247
+ ```
248
+
249
+ ---
250
+
251
+ ### Task 3: Push branch, open PR
252
+
253
+ **Files:** none (git/gh only)
254
+
255
+ - [x] **Step 1: Push and open the PR**
256
+
257
+ ```bash
258
+ git push -u origin fix/lint-fresh-vault-and-windows-basename
259
+ gh pr create \
260
+ --title "fix: lint ENOENT on fresh vaults (#203) + Windows capture basename (#204)" \
261
+ --body "Fixes #203. Fixes #204.
262
+
263
+ Two one-line fixes with regression tests:
264
+ - wiki_lint: mkdirSync the gitignored .discoveries dir before persisting the gap snapshot (mirrors the autoFix report write in the same function).
265
+ - wiki_capture_source: derive the original artifact name with platform basename instead of split('/').pop() so Windows absolute paths no longer embed a drive colon in the file name."
266
+ ```
267
+
268
+ Expected: PR created; `Fixes #203` / `Fixes #204` auto-close the issues on merge.
269
+
270
+ ---
271
+
272
+ ## Setup (before Task 1)
273
+
274
+ - [x] **Step 0: Branch from main**
275
+
276
+ Working tree is clean (verified 2026-09-02); current branch is `qmd-phase-1` — do the work on a dedicated branch off `main`:
277
+
278
+ ```bash
279
+ git checkout main
280
+ git pull --ff-only
281
+ git checkout -b fix/lint-fresh-vault-and-windows-basename
282
+ ```
283
+
284
+ Expected: `Switched to a new branch 'fix/lint-fresh-vault-and-windows-basename'`.
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { bootstrapVault } from "./lib/bootstrap.js";
5
5
  import { registerWikiDashboardCommand } from "./lib/dashboard-command.js";
6
6
  import { installGuardrails } from "./lib/guardrails.js";
@@ -1,8 +1,8 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { KnowledgeDiagnostic } from "./knowledge-document.js";
4
- import { type ProjectionResult, appendEvent, rebuildMetadata } from "./metadata.js";
5
- import { type VaultPaths, ensureVaultStructure, fmtDate, writeJson } from "./utils.js";
4
+ import { appendEvent, type ProjectionResult, rebuildMetadata } from "./metadata.js";
5
+ import { ensureVaultStructure, fmtDate, type VaultPaths, writeJson } from "./utils.js";
6
6
  import { inspectWritableVault, readVaultConfig } from "./vault-format.js";
7
7
 
8
8
  export const WIKI_SCHEMA = [
@@ -6,9 +6,9 @@
6
6
  * from on-disk vault state (see lib/dashboard.ts) and rendered as plain
7
7
  * text through a Container + Text.
8
8
  */
9
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
- import { Container, Text, matchesKey } from "@mariozechner/pi-tui";
11
- import { type DashboardStats, collectDashboardStats } from "./dashboard.js";
9
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
10
+ import { Container, matchesKey, Text } from "@earendil-works/pi-tui";
11
+ import { collectDashboardStats, type DashboardStats } from "./dashboard.js";
12
12
  import type { Runtime } from "./runtime.js";
13
13
 
14
14
  type App = ExtensionAPI;
@@ -84,6 +84,12 @@ export class DashboardScreen extends Container {
84
84
  }
85
85
  }
86
86
 
87
+ // ponytail: overlay options matching pi-blackhole's config modal style
88
+ const OVERLAY_OPTS = {
89
+ overlay: true,
90
+ overlayOptions: { anchor: "center", width: "92%", maxHeight: "95%" } as const,
91
+ };
92
+
87
93
  /**
88
94
  * Register the /wiki-dashboard command.
89
95
  */
@@ -100,7 +106,7 @@ export function registerWikiDashboardCommand(pi: ExtensionAPI, runtime: Runtime)
100
106
  const stats = await collectDashboardStats(ctx.cwd);
101
107
  await ctx.ui.custom((_tui, _theme, _keybindings, close) => {
102
108
  return new DashboardScreen(renderStatsLines(stats), close);
103
- });
109
+ }, OVERLAY_OPTS);
104
110
  },
105
111
  });
106
112
  }
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * No writes, no LLM calls, no new files. Pure reader.
13
13
  */
14
- import { readFile, readdir, stat } from "node:fs/promises";
14
+ import { readdir, readFile, stat } from "node:fs/promises";
15
15
  import type { VaultPaths } from "./utils.js";
16
16
  import { readJson, resolveVaultPaths } from "./utils.js";
17
17
 
@@ -7,7 +7,7 @@ import { parseKnowledgeDocument } from "./knowledge-document.js";
7
7
  import type { Registry } from "./metadata.js";
8
8
  import type { LaunchCtx, Runtime } from "./runtime.js";
9
9
  import type { TaskConfig } from "./task-config.js";
10
- import { type VaultPaths, readJson, writeJson } from "./utils.js";
10
+ import { readJson, type VaultPaths, writeJson } from "./utils.js";
11
11
  import { assertWritableVault } from "./vault-format.js";
12
12
 
13
13
  /**
@@ -1,10 +1,10 @@
1
- import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
2
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
3
3
  import { scheduleReindex } from "./indexing.js";
4
4
  import { rebuildMetadataLight } from "./metadata.js";
5
5
  import type { Runtime } from "./runtime.js";
6
- import { isPathWithin, isProtectedPath, resolveVaultPaths } from "./utils.js";
7
6
  import type { VaultPaths } from "./utils.js";
7
+ import { isPathWithin, isProtectedPath, resolveVaultPaths } from "./utils.js";
8
8
  import { inspectVaultFormat, isGeneratedOkfPath } from "./vault-format.js";
9
9
 
10
10
  /**
@@ -1,14 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  /**
6
6
  * Host adapter for the two coding agents that can load this extension:
7
7
  *
8
- * - **pi** — `@mariozechner/pi-coding-agent`, config dir `.pi`
8
+ * - **pi** — `@earendil-works/pi-coding-agent`, config dir `.pi`
9
9
  * - **omp** — oh-my-pi (`@oh-my-pi/pi-coding-agent`), config dir `.omp`
10
10
  *
11
- * omp rewrites `@mariozechner/pi-*` (and bare `typebox`) imports onto its own
11
+ * omp rewrites `@earendil-works/pi-*` (and bare `typebox`) imports onto its own
12
12
  * bundled packages at load time (its `legacy-pi-compat.ts`), so the *module
13
13
  * graph* needs no changes. What does differ is the on-disk config layout:
14
14
  *
@@ -129,7 +129,7 @@ export function resolveProjectSettingsPath(cwd: string, host: HostKind = detectH
129
129
  *
130
130
  * Always writes to `settings.json` inside the agent dir — both hosts read it.
131
131
  */
132
- export function resolveGlobalSettingsPath(host: HostKind = detectHost()): string {
132
+ export function resolveGlobalSettingsPath(_host: HostKind = detectHost()): string {
133
133
  let agentDir = "";
134
134
  try {
135
135
  agentDir = getAgentDir();
@@ -1,28 +1,28 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import type { AgentTool } from "@mariozechner/pi-agent-core";
4
- import type { Api, Model } from "@mariozechner/pi-ai";
5
- import { Type } from "typebox";
3
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
4
+ import type { Api, Model } from "@earendil-works/pi-ai";
6
5
  import type { Static } from "typebox";
6
+ import { Type } from "typebox";
7
7
  import {
8
+ createKnowledgeDocument,
8
9
  type KnowledgeDiagnostic,
9
10
  type KnowledgeDocument,
10
- createKnowledgeDocument,
11
11
  patchKnowledgeDocument,
12
12
  readKnowledgeDocumentFile,
13
13
  serializeKnowledgeDocument,
14
14
  writeKnowledgeDocumentFile,
15
15
  } from "./knowledge-document.js";
16
16
  import {
17
- type WikilinkValidationMode,
18
17
  auditWikilinks,
19
18
  buildWikilinkIndex,
19
+ type WikilinkValidationMode,
20
20
  } from "./knowledge-links.js";
21
21
  import { appendEvent, rebuildMetadataLight } from "./metadata.js";
22
22
  import { runSubAgent } from "./subagent.js";
23
23
  import { resolveWikilinkValidation } from "./task-config.js";
24
- import { type VaultPaths, fmtDate, readJson, slugify } from "./utils.js";
25
- import { VaultWriteError, assertWritableVault } from "./vault-format.js";
24
+ import { fmtDate, readJson, slugify, type VaultPaths } from "./utils.js";
25
+ import { assertWritableVault, VaultWriteError } from "./vault-format.js";
26
26
 
27
27
  /**
28
28
  * Background ingest synthesis (issue #65, part of epic #63).
@@ -225,7 +225,7 @@ function resolveMarkdownTarget(
225
225
  // Convert decoded backslashes to /
226
226
  const normalized = decoded.map((s) => s.replace(/\\/g, "/")).join("/");
227
227
 
228
- const sourceDir = sourceId.replace(/[^\/]*$/, ""); // directory of source
228
+ const sourceDir = sourceId.replace(/[^/]*$/, ""); // directory of source
229
229
  const parts = normalized.split("/");
230
230
  const isRootRelative = normalized.startsWith("/");
231
231
 
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
- constants,
4
3
  chmodSync,
5
4
  closeSync,
5
+ constants,
6
6
  existsSync,
7
7
  fstatSync,
8
8
  fsyncSync,
@@ -10,8 +10,8 @@ import {
10
10
  lstatSync,
11
11
  mkdirSync,
12
12
  openSync,
13
- readFileSync,
14
13
  readdirSync,
14
+ readFileSync,
15
15
  renameSync,
16
16
  unlinkSync,
17
17
  utimesSync,
@@ -19,14 +19,14 @@ import {
19
19
  } from "node:fs";
20
20
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
21
  import {
22
+ createKnowledgeDocument,
22
23
  type DiagnosticCode,
23
24
  type KnowledgeDiagnostic,
24
- createKnowledgeDocument,
25
25
  parseKnowledgeDocument,
26
26
  parseMarkdownFrontmatter,
27
27
  serializeKnowledgeDocument,
28
28
  } from "./knowledge-document.js";
29
- import { type VaultPaths, readJson } from "./utils.js";
29
+ import { readJson, type VaultPaths } from "./utils.js";
30
30
  import { discoverKnowledgeDocuments, inspectVaultFormat } from "./vault-format.js";
31
31
 
32
32
  const RECOVERABLE = new Set<DiagnosticCode>([
@@ -3,8 +3,8 @@ import {
3
3
  existsSync,
4
4
  lstatSync,
5
5
  mkdirSync,
6
- readFileSync,
7
6
  readdirSync,
7
+ readFileSync,
8
8
  renameSync,
9
9
  rmSync,
10
10
  writeFileSync,
@@ -12,11 +12,11 @@ import {
12
12
  import { dirname, join } from "node:path";
13
13
  import type { KnowledgeDiagnostic, KnowledgeDocument } from "./knowledge-document.js";
14
14
  import {
15
- type WikilinkIndex,
16
15
  buildResolvedBacklinks,
17
16
  buildWikilinkIndex,
17
+ type WikilinkIndex,
18
18
  } from "./knowledge-links.js";
19
- import { type VaultPaths, isPathWithin, readJson } from "./utils.js";
19
+ import { isPathWithin, readJson, type VaultPaths } from "./utils.js";
20
20
  import {
21
21
  assertWritableVault,
22
22
  compareCodePoint,
@@ -1,6 +1,15 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ Container,
4
+ matchesKey,
5
+ type SelectItem,
6
+ SelectList,
7
+ type SelectListTheme,
8
+ Spacer,
9
+ Text,
10
+ } from "@earendil-works/pi-tui";
2
11
  import type { Runtime } from "./runtime.js";
3
- import { type TaskConfig, parseModelRef, persistTaskModel } from "./task-config.js";
12
+ import { parseModelRef, persistTaskModel, type TaskConfig } from "./task-config.js";
4
13
 
5
14
  /**
6
15
  * Model selection surface for the wiki background lane (issue #69, epic #63).
@@ -25,6 +34,12 @@ interface ModelLike {
25
34
  /** Words that clear the override and revert to the session model. */
26
35
  const CLEAR_WORDS = new Set(["session", "default", "reset", "clear", "none", "unset"]);
27
36
 
37
+ /** Sentinel value for the "use session model" picker row — not a valid provider/id. */
38
+ const SESSION_VALUE = "__session__";
39
+
40
+ /** Same window size as pi's built-in `/model` picker. */
41
+ const MAX_VISIBLE = 10;
42
+
28
43
  /** The status-bar key for the active-model label (so we can update it in place). */
29
44
  export const MODEL_STATUS_KEY = "llm-wiki-model";
30
45
 
@@ -43,6 +58,85 @@ function modelRef(m: ModelLike): string {
43
58
  return `${m.provider}/${m.id}`;
44
59
  }
45
60
 
61
+ function buildSelectTheme(theme: Theme): SelectListTheme {
62
+ return {
63
+ selectedPrefix: (text) => theme.fg("accent", text),
64
+ selectedText: (text) => theme.fg("accent", text),
65
+ description: (text) => theme.fg("dim", text),
66
+ scrollInfo: (text) => theme.fg("dim", text),
67
+ noMatch: (text) => theme.fg("muted", text),
68
+ };
69
+ }
70
+
71
+ function buildPickerItems(
72
+ pool: ModelLike[],
73
+ currentRef: string | undefined,
74
+ ): { items: SelectItem[]; selectedIndex: number } {
75
+ const items: SelectItem[] = [
76
+ {
77
+ value: SESSION_VALUE,
78
+ label: "↩ Use session model (clear override)",
79
+ description: currentRef ? undefined : "current",
80
+ },
81
+ ];
82
+ let selectedIndex = 0;
83
+ for (const m of pool) {
84
+ const ref = modelRef(m);
85
+ const isCurrent = currentRef === ref;
86
+ items.push({
87
+ value: ref,
88
+ label: ref,
89
+ description: isCurrent ? "current" : undefined,
90
+ });
91
+ if (isCurrent) selectedIndex = items.length - 1;
92
+ }
93
+ return { items, selectedIndex };
94
+ }
95
+
96
+ /** Editor-dock picker; `ui.custom` without overlay replaces the input slot like `/model`. */
97
+ class ModelPickerScreen extends Container {
98
+ private list: SelectList;
99
+ private doneFn: (result?: string) => void;
100
+ private closed = false;
101
+
102
+ constructor(
103
+ title: string,
104
+ items: SelectItem[],
105
+ selectedIndex: number,
106
+ theme: Theme,
107
+ done: (result?: string) => void,
108
+ ) {
109
+ super();
110
+ this.doneFn = done;
111
+ this.addChild(new Text(title, 0, 0));
112
+ this.addChild(new Spacer(1));
113
+ this.list = new SelectList(
114
+ items,
115
+ Math.min(MAX_VISIBLE, Math.max(items.length, 1)),
116
+ buildSelectTheme(theme),
117
+ );
118
+ this.list.setSelectedIndex(selectedIndex);
119
+ this.list.onSelect = (item) => this.finish(item.value);
120
+ this.list.onCancel = () => this.finish(undefined);
121
+ this.addChild(this.list);
122
+ }
123
+
124
+ handleInput(data: string): void {
125
+ // matchesKey covers kitty CSI-u Esc; SelectList cancel is the fallback.
126
+ if (matchesKey(data, "escape")) {
127
+ this.finish(undefined);
128
+ return;
129
+ }
130
+ this.list.handleInput(data);
131
+ }
132
+
133
+ private finish(result?: string): void {
134
+ if (this.closed) return;
135
+ this.closed = true;
136
+ this.doneFn(result);
137
+ }
138
+ }
139
+
46
140
  /**
47
141
  * Register the `/wiki-model` slash command. Lets the user view the active
48
142
  * background task model and choose another (or revert to the session model).
@@ -108,15 +202,31 @@ export function registerWikiModelCommand(pi: ExtensionAPI, runtime: Runtime): vo
108
202
  return;
109
203
  }
110
204
 
205
+ if (typeof ctx.ui.custom !== "function") {
206
+ ctx.ui.notify(
207
+ `LLM Wiki: this host does not support the model picker. Pass provider/id (e.g. anthropic/claude-haiku) or "session". Active: ${current}.`,
208
+ "warning",
209
+ );
210
+ return;
211
+ }
212
+
111
213
  const available = (ctx.modelRegistry.getAvailable() as ModelLike[]) ?? [];
112
214
  const pool = available.length > 0 ? available : (ctx.modelRegistry.getAll() as ModelLike[]);
113
- const sessionOption = "↩ Use session model (clear override)";
114
- const options = [sessionOption, ...pool.map(modelRef)];
215
+ const currentRef = runtime.config.taskModel ? modelRef(runtime.config.taskModel) : undefined;
216
+ const { items, selectedIndex } = buildPickerItems(pool, currentRef);
115
217
 
116
- const picked = await ctx.ui.select(`Wiki background model (current: ${current})`, options);
218
+ const picked = await ctx.ui.custom<string | undefined>((_tui, theme, _keybindings, done) => {
219
+ return new ModelPickerScreen(
220
+ `Wiki background model (current: ${current})`,
221
+ items,
222
+ selectedIndex,
223
+ theme,
224
+ done,
225
+ );
226
+ });
117
227
  if (picked === undefined) return; // cancelled
118
228
 
119
- if (picked === sessionOption) {
229
+ if (picked === SESSION_VALUE) {
120
230
  apply(undefined);
121
231
  return;
122
232
  }