@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,378 @@
|
|
|
1
|
+
# Custom Wiki Page Types 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:** Allow users to register custom page types via settings, so `wiki_ensure_page` supports project-specific taxonomies alongside the 7 built-in types.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add `customTypes` to `TaskConfig` (existing settings surface). At runtime, merge custom types into `wiki_ensure_page`'s folder map. Custom types get a generic page template (no per-type templates — YAGNI). Works transparently for both pi and oh-my-pi via the existing `settings.json` / `config.yml` pipeline.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Vitest
|
|
10
|
+
|
|
11
|
+
**Roadmap:** None
|
|
12
|
+
|
|
13
|
+
**Phase:** Single-plan implementation
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Map
|
|
18
|
+
|
|
19
|
+
| File | Change |
|
|
20
|
+
|------|--------|
|
|
21
|
+
| `extensions/llm-wiki/lib/task-config.ts:33` | Add `customTypes` field to `TaskConfig` interface |
|
|
22
|
+
| `extensions/llm-wiki/lib/task-config.ts:225` | Add parsing block in `readNamespacedConfig` |
|
|
23
|
+
| `extensions/llm-wiki/lib/task-config.ts:488` | Add `"customTypes"` to `KNOWN_KEYS` array |
|
|
24
|
+
| `extensions/llm-wiki/lib/tools.ts:520` | Update type parameter description |
|
|
25
|
+
| `extensions/llm-wiki/lib/tools.ts:540` | Merge config custom types into `folderMap` |
|
|
26
|
+
| `test/custom-types.test.ts` | New test file for custom type behavior |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Task 1: Add `customTypes` to TaskConfig
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Modify: `extensions/llm-wiki/lib/task-config.ts:33-165` (interface)
|
|
34
|
+
- Modify: `extensions/llm-wiki/lib/task-config.ts:225-320` (readNamespacedConfig)
|
|
35
|
+
- Modify: `extensions/llm-wiki/lib/task-config.ts:488` (KNOWN_KEYS)
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 1: Add the field to TaskConfig interface**
|
|
38
|
+
|
|
39
|
+
Insert after the `wikilinkValidation` field (line ~165):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
/**
|
|
43
|
+
* User-defined page types for wiki_ensure_page (issue #169). Merges with
|
|
44
|
+
* the 7 built-in types (entity, concept, synthesis, analysis, requirement,
|
|
45
|
+
* skill, case). Each key is the type name; each value is the folder name
|
|
46
|
+
* inside wiki/. Example: { "decision": "decisions", "metric": "metrics" }
|
|
47
|
+
* → wiki_ensure_page(type="decision") creates wiki/decisions/<slug>.md.
|
|
48
|
+
* Custom types use a generic page template (no per-type templates).
|
|
49
|
+
*/
|
|
50
|
+
customTypes?: Record<string, string>;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- [ ] **Step 2: Add parsing in `readNamespacedConfig`**
|
|
54
|
+
|
|
55
|
+
Insert before the `return out;` line (line ~318), after the `wikilinkValidation` block:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const ct = section.customTypes;
|
|
59
|
+
if (ct && typeof ct === "object" && !Array.isArray(ct)) {
|
|
60
|
+
const entries = Object.entries(ct as Record<string, unknown>);
|
|
61
|
+
const valid: Record<string, string> = {};
|
|
62
|
+
for (const [k, v] of entries) {
|
|
63
|
+
if (typeof k === "string" && typeof v === "string" && k && v) {
|
|
64
|
+
valid[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(valid).length) out.customTypes = valid;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This validates that customTypes is a flat `Record<string, string>` — rejects arrays, nested objects, and non-string values.
|
|
72
|
+
|
|
73
|
+
- [ ] **Step 3: Add to KNOWN_KEYS**
|
|
74
|
+
|
|
75
|
+
In the `KNOWN_KEYS` array (line ~488), add `"customTypes"` after `"wikilinkValidation"`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const KNOWN_KEYS = [
|
|
79
|
+
"taskModel",
|
|
80
|
+
"embeddingProvider",
|
|
81
|
+
"embeddingModel",
|
|
82
|
+
"embeddingBaseUrl",
|
|
83
|
+
"embeddingApiKey",
|
|
84
|
+
"embeddingApiKeyEnv",
|
|
85
|
+
"semanticWeight",
|
|
86
|
+
"recallLinksThreshold",
|
|
87
|
+
"recallSkillInlineMax",
|
|
88
|
+
"notices",
|
|
89
|
+
"ambientPersonalVault",
|
|
90
|
+
"trajectories",
|
|
91
|
+
"synthesisLanguage",
|
|
92
|
+
"synthesisMaxTokens",
|
|
93
|
+
"wikilinkValidation",
|
|
94
|
+
"customTypes",
|
|
95
|
+
] as const;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 4: Commit**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git add extensions/llm-wiki/lib/task-config.ts
|
|
102
|
+
git commit -m "feat(config): add customTypes to TaskConfig with readNamespacedConfig parsing"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Task 2: Merge custom types into wiki_ensure_page
|
|
108
|
+
|
|
109
|
+
**Files:**
|
|
110
|
+
- Modify: `extensions/llm-wiki/lib/tools.ts:520` (type description)
|
|
111
|
+
- Modify: `extensions/llm-wiki/lib/tools.ts:540-564` (folderMap + type cast)
|
|
112
|
+
|
|
113
|
+
- [ ] **Step 1: Load config and merge folderMap**
|
|
114
|
+
|
|
115
|
+
Replace the hardcoded `folderMap` and type cast (lines ~540-564) with:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const config = loadTaskConfig(ctx.cwd);
|
|
119
|
+
const builtInFolderMap: Record<string, string> = {
|
|
120
|
+
entity: "entities",
|
|
121
|
+
concept: "concepts",
|
|
122
|
+
synthesis: "syntheses",
|
|
123
|
+
analysis: "analyses",
|
|
124
|
+
requirement: "requirements",
|
|
125
|
+
skill: "skills",
|
|
126
|
+
case: "cases",
|
|
127
|
+
};
|
|
128
|
+
const folderMap = { ...builtInFolderMap, ...config.customTypes };
|
|
129
|
+
const type = params.type as string;
|
|
130
|
+
const slug = slugify(params.title);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Remove the old `const type = params.type as "entity" | "concept" | ...` cast — it's now just `string` since custom types are valid.
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 2: Update the type parameter description**
|
|
136
|
+
|
|
137
|
+
Replace the hardcoded type description (line ~520):
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
type: Type.String({
|
|
141
|
+
description:
|
|
142
|
+
"Page type: entity | concept | synthesis | analysis | requirement | skill | case (built-in) or any user-defined type from llm-wiki.customTypes config",
|
|
143
|
+
}),
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 3: Verify buildPageBody fallback**
|
|
147
|
+
|
|
148
|
+
`buildPageBody` already has a generic fallback at the end (after the `requirement` block) that returns a basic template for any unrecognized type. No code change needed — custom types hit this existing fallback. Just verify it exists by reading the function end.
|
|
149
|
+
|
|
150
|
+
- [ ] **Step 4: Run existing tests to verify no regression**
|
|
151
|
+
|
|
152
|
+
Run: `pnpm test`
|
|
153
|
+
Expected: All 699+ tests pass
|
|
154
|
+
|
|
155
|
+
- [ ] **Step 5: Commit**
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git add extensions/llm-wiki/lib/tools.ts
|
|
159
|
+
git commit -m "feat(wiki_ensure_page): merge customTypes from config into folder map"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Task 3: Tests for custom types
|
|
165
|
+
|
|
166
|
+
**Files:**
|
|
167
|
+
- Create: `test/custom-types.test.ts`
|
|
168
|
+
|
|
169
|
+
- [ ] **Step 1: Write the test file**
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
173
|
+
import { join } from "node:path";
|
|
174
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
175
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
176
|
+
import { registerWikiEnsurePage } from "../extensions/llm-wiki/lib/tools.js";
|
|
177
|
+
import { ensureVaultStructure, getVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
178
|
+
|
|
179
|
+
interface Tool {
|
|
180
|
+
execute: (
|
|
181
|
+
id: string,
|
|
182
|
+
params: Record<string, unknown>,
|
|
183
|
+
s: undefined,
|
|
184
|
+
u: undefined,
|
|
185
|
+
ctx: unknown,
|
|
186
|
+
) => Promise<{
|
|
187
|
+
isError?: boolean;
|
|
188
|
+
content: Array<{ text: string }>;
|
|
189
|
+
details: Record<string, unknown>;
|
|
190
|
+
}>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function capture(fn: (pi: ExtensionAPI) => void): Tool {
|
|
194
|
+
let tool: Tool | undefined;
|
|
195
|
+
const pi = {
|
|
196
|
+
registerTool: (def: unknown) => {
|
|
197
|
+
tool = def as Tool;
|
|
198
|
+
},
|
|
199
|
+
} as unknown as ExtensionAPI;
|
|
200
|
+
fn(pi);
|
|
201
|
+
if (!tool) throw new Error("tool not registered");
|
|
202
|
+
return tool;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let wikiDir: string;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
wikiDir = join(
|
|
209
|
+
import.meta.dirname,
|
|
210
|
+
"..",
|
|
211
|
+
"tmp",
|
|
212
|
+
`ct-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
213
|
+
);
|
|
214
|
+
const llm = join(wikiDir, ".llm-wiki");
|
|
215
|
+
for (const d of ["wiki/entities", "wiki/concepts", "wiki/sources", "meta", "outputs"]) {
|
|
216
|
+
mkdirSync(join(llm, d), { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
writeFileSync(join(llm, "config.json"), JSON.stringify({ topic: "Test", mode: "personal" }));
|
|
219
|
+
ensureVaultStructure(getVaultPaths(wikiDir));
|
|
220
|
+
writeFileSync(
|
|
221
|
+
join(llm, "meta", "registry.json"),
|
|
222
|
+
JSON.stringify({ version: "1.0", last_updated: "", pages: {} }),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
afterEach(() => {
|
|
227
|
+
try {
|
|
228
|
+
rmSync(wikiDir, { recursive: true, force: true });
|
|
229
|
+
} catch {}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
function writeSettings(customTypes: Record<string, string>): void {
|
|
233
|
+
const cfg = join(wikiDir, ".pi");
|
|
234
|
+
mkdirSync(cfg, { recursive: true });
|
|
235
|
+
writeFileSync(
|
|
236
|
+
join(cfg, "settings.json"),
|
|
237
|
+
JSON.stringify({ "llm-wiki": { customTypes } }),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
describe("wiki_ensure_page custom types", () => {
|
|
242
|
+
it("creates a custom type in the correct folder", async () => {
|
|
243
|
+
writeSettings({ decision: "decisions" });
|
|
244
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
245
|
+
const res = await tool.execute(
|
|
246
|
+
"t",
|
|
247
|
+
{ type: "decision", title: "Auth Architecture" },
|
|
248
|
+
undefined,
|
|
249
|
+
undefined,
|
|
250
|
+
{ cwd: wikiDir, hasUI: false },
|
|
251
|
+
);
|
|
252
|
+
expect(res.isError).toBeFalsy();
|
|
253
|
+
const expected = join(getVaultPaths(wikiDir).wiki, "decisions", "auth-architecture.md");
|
|
254
|
+
expect(existsSync(expected)).toBe(true);
|
|
255
|
+
expect(res.details.created).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("built-in types still work alongside custom types", async () => {
|
|
259
|
+
writeSettings({ decision: "decisions" });
|
|
260
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
261
|
+
const res = await tool.execute(
|
|
262
|
+
"t",
|
|
263
|
+
{ type: "entity", title: "Test Entity" },
|
|
264
|
+
undefined,
|
|
265
|
+
undefined,
|
|
266
|
+
{ cwd: wikiDir, hasUI: false },
|
|
267
|
+
);
|
|
268
|
+
expect(res.isError).toBeFalsy();
|
|
269
|
+
const expected = join(getVaultPaths(wikiDir).wiki, "entities", "test-entity.md");
|
|
270
|
+
expect(existsSync(expected)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("custom type uses generic template when no content provided", async () => {
|
|
274
|
+
writeSettings({ metric: "metrics" });
|
|
275
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
276
|
+
const res = await tool.execute(
|
|
277
|
+
"t",
|
|
278
|
+
{ type: "metric", title: "API Latency" },
|
|
279
|
+
undefined,
|
|
280
|
+
undefined,
|
|
281
|
+
{ cwd: wikiDir, hasUI: false },
|
|
282
|
+
);
|
|
283
|
+
expect(res.isError).toBeFalsy();
|
|
284
|
+
const file = join(getVaultPaths(wikiDir).wiki, "metrics", "api-latency.md");
|
|
285
|
+
const body = readFileSync(file, "utf-8");
|
|
286
|
+
expect(body).toContain("# API Latency");
|
|
287
|
+
expect(body).toContain("## Links");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("custom type with explicit content writes that content", async () => {
|
|
291
|
+
writeSettings({ decision: "decisions" });
|
|
292
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
293
|
+
const res = await tool.execute(
|
|
294
|
+
"t",
|
|
295
|
+
{ type: "decision", title: "Use Postgres", content: "# Use Postgres\n\nWe chose Postgres." },
|
|
296
|
+
undefined,
|
|
297
|
+
undefined,
|
|
298
|
+
{ cwd: wikiDir, hasUI: false },
|
|
299
|
+
);
|
|
300
|
+
expect(res.isError).toBeFalsy();
|
|
301
|
+
const file = join(getVaultPaths(wikiDir).wiki, "decisions", "use-postgres.md");
|
|
302
|
+
const body = readFileSync(file, "utf-8");
|
|
303
|
+
expect(body).toContain("We chose Postgres.");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("undefined customTypes (no config) falls back to built-ins only", async () => {
|
|
307
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
308
|
+
const res = await tool.execute(
|
|
309
|
+
"t",
|
|
310
|
+
{ type: "concept", title: "Test Concept" },
|
|
311
|
+
undefined,
|
|
312
|
+
undefined,
|
|
313
|
+
{ cwd: wikiDir, hasUI: false },
|
|
314
|
+
);
|
|
315
|
+
expect(res.isError).toBeFalsy();
|
|
316
|
+
const expected = join(getVaultPaths(wikiDir).wiki, "concepts", "test-concept.md");
|
|
317
|
+
expect(existsSync(expected)).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("unrecognized type without config falls back to concepts folder", async () => {
|
|
321
|
+
const tool = capture((pi) => registerWikiEnsurePage(pi));
|
|
322
|
+
const res = await tool.execute(
|
|
323
|
+
"t",
|
|
324
|
+
{ type: "nonexistent", title: "Fallback Page" },
|
|
325
|
+
undefined,
|
|
326
|
+
undefined,
|
|
327
|
+
{ cwd: wikiDir, hasUI: false },
|
|
328
|
+
);
|
|
329
|
+
expect(res.isError).toBeFalsy();
|
|
330
|
+
// folderMap[type] || "concepts" fallback
|
|
331
|
+
const expected = join(getVaultPaths(wikiDir).wiki, "concepts", "fallback-page.md");
|
|
332
|
+
expect(existsSync(expected)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 2: Run the new tests**
|
|
338
|
+
|
|
339
|
+
Run: `pnpm vitest run test/custom-types.test.ts`
|
|
340
|
+
Expected: All 6 tests pass
|
|
341
|
+
|
|
342
|
+
- [ ] **Step 3: Run full test suite**
|
|
343
|
+
|
|
344
|
+
Run: `pnpm test`
|
|
345
|
+
Expected: All tests pass (no regression)
|
|
346
|
+
|
|
347
|
+
- [ ] **Step 4: Commit**
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
git add test/custom-types.test.ts
|
|
351
|
+
git commit -m "test: custom wiki page types via config"
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Task 4: Final verification
|
|
357
|
+
|
|
358
|
+
- [ ] **Step 1: Typecheck**
|
|
359
|
+
|
|
360
|
+
Run: `pnpm typecheck`
|
|
361
|
+
Expected: No errors
|
|
362
|
+
|
|
363
|
+
- [ ] **Step 2: Lint**
|
|
364
|
+
|
|
365
|
+
Run: `pnpm lint`
|
|
366
|
+
Expected: No errors
|
|
367
|
+
|
|
368
|
+
- [ ] **Step 3: Full test suite one more time**
|
|
369
|
+
|
|
370
|
+
Run: `pnpm test`
|
|
371
|
+
Expected: All tests green
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 4: If all green, push branch and open PR**
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
git push -u origin feat/custom-wiki-page-types
|
|
377
|
+
gh pr create --title "feat: support custom wiki page types via config (closes #169)" --body "..."
|
|
378
|
+
```
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Keyless Provider Ingest Fix — 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 `wiki_ingest` background synthesis silently no-oping for keyless local providers (llama.cpp, self-hosted models).
|
|
6
|
+
|
|
7
|
+
**Architecture:** One-line logic fix in `Runtime.resolveModel()` + one new test. The `!auth.apiKey` check in the auth gate wrongly rejects providers where `getApiKeyAndHeaders` returns `{ ok: true }` without an `apiKey` field — the correct response for a server that needs no key.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Vitest
|
|
10
|
+
|
|
11
|
+
**Roadmap:** None
|
|
12
|
+
|
|
13
|
+
**Phase:** Single-plan implementation
|
|
14
|
+
|
|
15
|
+
**Issue:** [#174](https://github.com/zosmaai/pi-llm-wiki/issues/174)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Context
|
|
20
|
+
|
|
21
|
+
`Runtime.resolveModel()` at `extensions/llm-wiki/lib/runtime.ts:135-140` gates background work on API key presence:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
25
|
+
if (!auth.ok || !auth.apiKey) { // ← bug: || !auth.apiKey
|
|
26
|
+
const provider = (model as { provider?: string }).provider ?? "unknown";
|
|
27
|
+
return { ok: false, reason: `no API key for provider "${provider}"` };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For keyless providers, the host's `getApiKeyAndHeaders` returns `{ ok: true, headers: {...} }` — `ok: true` because auth is satisfied, no `apiKey` because the server doesn't need one. But `!auth.apiKey` is `true` (undefined is falsy), so `resolveModel` returns `ok: false`.
|
|
33
|
+
|
|
34
|
+
Downstream in `tools.ts:408`:
|
|
35
|
+
```ts
|
|
36
|
+
const resolved = await runtime.resolveModel(ctx, override);
|
|
37
|
+
if (resolved.ok) {
|
|
38
|
+
// launch background synthesis ... ← NEVER REACHED for keyless providers
|
|
39
|
+
}
|
|
40
|
+
// falls through to synchronous "📥 N source(s) ready" — no synthesis, no ingest event
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The synchronous fallback looks like success to the agent and user, so the bug is invisible.
|
|
44
|
+
|
|
45
|
+
**The fix:** Remove `|| !auth.apiKey`. The `!auth.ok` clause alone is sufficient — when a provider *requires* auth and has no key, the host returns `{ ok: false }`. When a provider needs no key, `{ ok: true }` without `apiKey` is correct.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Files
|
|
50
|
+
|
|
51
|
+
| Action | File | Purpose |
|
|
52
|
+
|--------|------|---------|
|
|
53
|
+
| Modify | `extensions/llm-wiki/lib/runtime.ts:135-141` | Fix the auth gate |
|
|
54
|
+
| Modify | `test/runtime.test.ts` — `makeRegistry` + new test | Add keyless provider test |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Task 1: Fix the auth gate in resolveModel
|
|
59
|
+
|
|
60
|
+
**Files:**
|
|
61
|
+
- Modify: `extensions/llm-wiki/lib/runtime.ts:135-140`
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 1: Read the current code to confirm the line numbers**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
awk 'NR>=133 && NR<=143' extensions/llm-wiki/lib/runtime.ts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Expected: lines 135-136 show `if (!auth.ok || !auth.apiKey)` and line 141 shows `apiKey: auth.apiKey`.
|
|
70
|
+
|
|
71
|
+
- [ ] **Step 2: Apply the fix**
|
|
72
|
+
|
|
73
|
+
Edit `extensions/llm-wiki/lib/runtime.ts` — replace lines 135-140:
|
|
74
|
+
|
|
75
|
+
Old:
|
|
76
|
+
```ts
|
|
77
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
78
|
+
if (!auth.ok || !auth.apiKey) {
|
|
79
|
+
const provider = (model as { provider?: string }).provider ?? "unknown";
|
|
80
|
+
return { ok: false, reason: `no API key for provider "${provider}"` };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
New:
|
|
86
|
+
```ts
|
|
87
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
88
|
+
if (!auth.ok) {
|
|
89
|
+
const provider = (model as { provider?: string }).provider ?? "unknown";
|
|
90
|
+
return { ok: false, reason: `no API key for provider "${provider}"` };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, model, apiKey: auth.apiKey ?? "", headers: auth.headers };
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two changes:
|
|
96
|
+
1. `|| !auth.apiKey` removed from the condition (line 136)
|
|
97
|
+
2. `auth.apiKey` → `auth.apiKey ?? ""` on the return (line 140) — defaults to empty string for keyless providers
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 3: Run typecheck to verify no type errors**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm typecheck
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Expected: clean exit (no errors). The `apiKey` field is typed `string` in `ResolveResult`; `?? ""` satisfies it.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Task 2: Update tests — existing + new keyless provider case
|
|
110
|
+
|
|
111
|
+
**Files:**
|
|
112
|
+
- Modify: `test/runtime.test.ts` — `makeRegistry` helper + new test
|
|
113
|
+
|
|
114
|
+
- [ ] **Step 1: Read the existing test to confirm it still applies**
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
awk 'NR>=176 && NR<=181' test/runtime.test.ts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Expected: the test uses `authOk: false` — this tests "provider requires auth but has no key" → should still be rejected. This test is correct and stays unchanged.
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 2: Fix `makeRegistry` to support explicit `apiKey: undefined`**
|
|
123
|
+
|
|
124
|
+
The current `makeRegistry` defaults `apiKey` to `"key-123"` when `authOk: true`, so passing `apiKey: undefined` doesn't produce the keyless scenario. Fix the helper to use `"apiKey" in opts` to distinguish "not passed" from "explicitly undefined":
|
|
125
|
+
|
|
126
|
+
Old:
|
|
127
|
+
```ts
|
|
128
|
+
function makeRegistry(opts: {
|
|
129
|
+
found?: unknown;
|
|
130
|
+
authOk?: boolean;
|
|
131
|
+
apiKey?: string;
|
|
132
|
+
}) {
|
|
133
|
+
return {
|
|
134
|
+
find: (_p: string, _i: string) => opts.found,
|
|
135
|
+
getApiKeyAndHeaders: async (_m: unknown) => ({
|
|
136
|
+
ok: opts.authOk ?? true,
|
|
137
|
+
apiKey: opts.apiKey ?? (opts.authOk === false ? undefined : "key-123"),
|
|
138
|
+
headers: { "x-test": "1" },
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
New:
|
|
145
|
+
```ts
|
|
146
|
+
function makeRegistry(opts: {
|
|
147
|
+
found?: unknown;
|
|
148
|
+
authOk?: boolean;
|
|
149
|
+
apiKey?: string | undefined;
|
|
150
|
+
_apiKeySet?: boolean; // true when caller explicitly passed apiKey
|
|
151
|
+
}) {
|
|
152
|
+
const apiKeyExplicit = opts._apiKeySet ?? false;
|
|
153
|
+
return {
|
|
154
|
+
find: (_p: string, _i: string) => opts.found,
|
|
155
|
+
getApiKeyAndHeaders: async (_m: unknown) => ({
|
|
156
|
+
ok: opts.authOk ?? true,
|
|
157
|
+
// When apiKey was explicitly passed (even as undefined), use it as-is.
|
|
158
|
+
// When omitted, default: "key-123" for auth-ok, undefined for auth-fail.
|
|
159
|
+
apiKey: apiKeyExplicit ? opts.apiKey : (opts.authOk === false ? undefined : "key-123"),
|
|
160
|
+
headers: { "x-test": "1" },
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
And update all existing call sites to pass `_apiKeySet: false` (or omit it — the default handles existing tests). Only the new keyless test passes `_apiKeySet: true`.
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 3: Add the new keyless provider test**
|
|
169
|
+
|
|
170
|
+
After the existing "returns ok:false when the provider has no API key" test (line 181), add:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
it("accepts a keyless provider (authOk, no apiKey)", async () => {
|
|
174
|
+
const rt = new Runtime();
|
|
175
|
+
const reg = makeRegistry({ found: undefined, authOk: true, apiKey: undefined, _apiKeySet: true });
|
|
176
|
+
const res = await rt.resolveModel({ model: SESSION_MODEL, modelRegistry: reg, hasUI: false });
|
|
177
|
+
expect(res.ok).toBe(true);
|
|
178
|
+
if (res.ok) {
|
|
179
|
+
expect(res.model).toBe(SESSION_MODEL);
|
|
180
|
+
expect(res.apiKey).toBe("");
|
|
181
|
+
expect(res.headers).toEqual({ "x-test": "1" });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This tests the exact scenario from #174: `getApiKeyAndHeaders` returns `{ ok: true, apiKey: undefined }` (keyless provider). The fix should make this pass.
|
|
187
|
+
|
|
188
|
+
- [ ] **Step 3: Run the test suite**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pnpm test
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Expected: all tests pass, including the new keyless provider test. The existing "no API key" test (authOk: false) still passes — it tests a different case.
|
|
195
|
+
|
|
196
|
+
- [ ] **Step 4: Run the full gate (typecheck + lint + test)**
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pnpm typecheck && pnpm lint && pnpm test
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Expected: all green.
|
|
203
|
+
|
|
204
|
+
- [ ] **Step 5: Commit**
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
git add extensions/llm-wiki/lib/runtime.ts test/runtime.test.ts
|
|
208
|
+
git commit -m "fix: accept keyless providers in resolveModel (closes #174)
|
|
209
|
+
|
|
210
|
+
remove the !auth.apiKey guard that wrongly rejected providers where
|
|
211
|
+
getApiKeyAndHeaders returns ok:true without an apiKey field (the correct
|
|
212
|
+
response for keyless local servers like llama.cpp). apiKey defaults to
|
|
213
|
+
\"\" for keyless providers, which local servers ignore.
|
|
214
|
+
|
|
215
|
+
adds a test for the keyless provider case (authOk, no apiKey)."
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### Task 3: Verify end-to-end
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 1: Run the full test suite one final time**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pnpm test
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Expected: 725+ tests pass (725 from last session + 1 new = 726).
|
|
229
|
+
|
|
230
|
+
- [ ] **Step 2: Check git diff to confirm minimal change**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git diff HEAD~1 --stat
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Expected: 2 files changed, ~3 lines removed, ~10 lines added (test).
|
|
237
|
+
|
|
238
|
+
- [ ] **Step 3: Verify no other callers of resolveModel are affected**
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
grep -rn "resolveModel" extensions/ --include="*.ts"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Expected: only `runtime.ts` (definition) and `tools.ts` (caller). Both are safe — `tools.ts` already handles `ok: true` with empty `apiKey` via `runSubAgent`.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## What was skipped
|
|
249
|
+
|
|
250
|
+
- **Docs update:** No user-facing docs change needed — this is a bug fix, not a feature. The `docs/configuration.md` already documents `taskModel` configuration. The fix makes the documented behavior actually work for keyless providers.
|
|
251
|
+
- **Changelog entry:** Will be captured in the PR description and release notes when merged.
|
|
252
|
+
|
|
253
|
+
## When to add
|
|
254
|
+
|
|
255
|
+
- **Robustness:** If a provider returns `{ ok: true, apiKey: "" }` explicitly (not just undefined), the `?? ""` still handles it correctly.
|
|
256
|
+
- **End-to-end test with real llama.cpp:** Not feasible in CI (needs a running local server), but the unit test covers the exact code path. Manual verification: point pi at a keyless local provider, run `wiki_capture_source` + `wiki_ingest`, confirm background synthesis launches.
|