@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.
- package/README.md +7 -0
- package/dist/extensions/llm-wiki/lib/dashboard-command.js +7 -2
- package/dist/extensions/llm-wiki/lib/dashboard.js +1 -1
- package/dist/extensions/llm-wiki/lib/guardrails.js +1 -1
- package/dist/extensions/llm-wiki/lib/host.js +2 -2
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +1 -1
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +1 -1
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +1 -1
- package/dist/extensions/llm-wiki/lib/metadata.js +1 -1
- package/dist/extensions/llm-wiki/lib/model-command.js +77 -4
- package/dist/extensions/llm-wiki/lib/recall.js +2 -2
- package/dist/extensions/llm-wiki/lib/runtime.js +1 -1
- package/dist/extensions/llm-wiki/lib/settings-command.js +7 -2
- package/dist/extensions/llm-wiki/lib/source-extractors.js +21 -5
- package/dist/extensions/llm-wiki/lib/source-packet.js +10 -2
- package/dist/extensions/llm-wiki/lib/subagent.js +1 -1
- package/dist/extensions/llm-wiki/lib/tools.js +4 -1
- package/dist/extensions/llm-wiki/lib/trajectory.js +1 -1
- package/dist/extensions/llm-wiki/lib/utils.js +1 -1
- package/dist/extensions/llm-wiki/lib/vault-format.js +1 -1
- package/dist/extensions/llm-wiki/lib/visible-status.js +1 -1
- package/dist/mcp/operations.js +1 -1
- package/docs/superpowers/plans/2026-08-29-custom-wiki-page-types.md +378 -0
- package/docs/superpowers/plans/2026-08-29-keyless-provider-ingest-fix.md +256 -0
- package/docs/superpowers/plans/2026-09-02-wiki-lint-fresh-vault-and-windows-capture-fixes.md +284 -0
- package/extensions/llm-wiki/index.ts +1 -1
- package/extensions/llm-wiki/lib/bootstrap.ts +2 -2
- package/extensions/llm-wiki/lib/dashboard-command.ts +10 -4
- package/extensions/llm-wiki/lib/dashboard.ts +1 -1
- package/extensions/llm-wiki/lib/embeddings.ts +1 -1
- package/extensions/llm-wiki/lib/guardrails.ts +3 -3
- package/extensions/llm-wiki/lib/host.ts +4 -4
- package/extensions/llm-wiki/lib/ingest-worker.ts +8 -8
- package/extensions/llm-wiki/lib/knowledge-links.ts +1 -1
- package/extensions/llm-wiki/lib/legacy-repair.ts +4 -4
- package/extensions/llm-wiki/lib/metadata.ts +3 -3
- package/extensions/llm-wiki/lib/model-command.ts +116 -6
- package/extensions/llm-wiki/lib/observation.ts +2 -2
- package/extensions/llm-wiki/lib/recall.ts +5 -5
- package/extensions/llm-wiki/lib/retro.ts +2 -2
- package/extensions/llm-wiki/lib/runtime.ts +2 -2
- package/extensions/llm-wiki/lib/settings-command.ts +11 -4
- package/extensions/llm-wiki/lib/source-extractors.ts +20 -12
- package/extensions/llm-wiki/lib/source-packet.ts +13 -5
- package/extensions/llm-wiki/lib/subagent.ts +2 -2
- package/extensions/llm-wiki/lib/task-config.ts +1 -1
- package/extensions/llm-wiki/lib/tools.ts +7 -4
- package/extensions/llm-wiki/lib/trajectories-command.ts +1 -1
- package/extensions/llm-wiki/lib/trajectory.ts +3 -3
- package/extensions/llm-wiki/lib/utils.ts +2 -2
- package/extensions/llm-wiki/lib/vault-format.ts +2 -2
- package/extensions/llm-wiki/lib/visible-status.ts +1 -1
- package/extensions/llm-wiki/lib/wiki-service.ts +1 -1
- package/mcp/operations.ts +4 -4
- 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 "@
|
|
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,
|
|
5
|
-
import {
|
|
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 "@
|
|
10
|
-
import { Container,
|
|
11
|
-
import { type DashboardStats
|
|
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 {
|
|
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,
|
|
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 {
|
|
2
|
-
import
|
|
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 "@
|
|
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** — `@
|
|
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 `@
|
|
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(
|
|
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
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { AgentTool } from "@
|
|
4
|
-
import type { Api, Model } from "@
|
|
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 {
|
|
25
|
-
import {
|
|
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(/[
|
|
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
|
|
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 {
|
|
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 "@
|
|
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 {
|
|
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
|
|
114
|
-
const
|
|
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.
|
|
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 ===
|
|
229
|
+
if (picked === SESSION_VALUE) {
|
|
120
230
|
apply(undefined);
|
|
121
231
|
return;
|
|
122
232
|
}
|