@zosmaai/pi-llm-wiki 0.11.2 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.de.md +2 -1
- package/README.es.md +2 -1
- package/README.fr.md +2 -1
- package/README.hi.md +2 -1
- package/README.ja.md +2 -1
- package/README.ko.md +2 -1
- package/README.md +89 -10
- package/README.pt.md +2 -1
- package/README.ru.md +2 -1
- package/README.zh.md +2 -1
- package/commands/wiki-digest.md +28 -0
- package/commands/wiki-discover.md +30 -0
- package/commands/wiki-ingest.md +36 -0
- package/commands/wiki-init.md +30 -0
- package/commands/wiki-lint.md +25 -0
- package/commands/wiki-query.md +37 -0
- package/commands/wiki-record.md +36 -0
- package/commands/wiki-req.md +55 -0
- package/commands/wiki-retro.md +34 -0
- package/commands/wiki-run.md +31 -0
- package/commands/wiki-skills.md +26 -0
- package/commands/wiki-status.md +16 -0
- package/dist/extensions/llm-wiki/lib/host.js +97 -0
- package/dist/extensions/llm-wiki/lib/observation.js +9 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +13 -1
- package/dist/extensions/llm-wiki/lib/task-config.js +74 -49
- package/dist/extensions/llm-wiki/lib/tools.js +10 -7
- package/dist/extensions/llm-wiki/lib/utils.js +59 -16
- package/dist/mcp/index.js +55 -3
- package/dist/mcp/operations.js +39 -3
- package/docs/api.md +9 -1
- package/docs/configuration.md +49 -8
- package/extensions/llm-wiki/index.ts +44 -6
- package/extensions/llm-wiki/lib/host.ts +125 -0
- package/extensions/llm-wiki/lib/observation.ts +14 -1
- package/extensions/llm-wiki/lib/runtime.ts +13 -1
- package/extensions/llm-wiki/lib/task-config.ts +115 -55
- package/extensions/llm-wiki/lib/tools.ts +10 -7
- package/extensions/llm-wiki/lib/utils.ts +55 -14
- package/mcp/index.ts +65 -2
- package/mcp/operations.ts +47 -4
- package/package.json +12 -1
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname
|
|
3
|
-
import {
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import {
|
|
5
|
+
type HostKind,
|
|
6
|
+
detectHost,
|
|
7
|
+
listGlobalSettingsFiles,
|
|
8
|
+
listProjectSettingsFiles,
|
|
9
|
+
resolveProjectSettingsPath,
|
|
10
|
+
} from "./host.js";
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Configuration for the background-task lane (issue #64, part of #63).
|
|
@@ -12,8 +19,11 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
|
12
19
|
*
|
|
13
20
|
* Resolution order (later wins):
|
|
14
21
|
* 1. built-in DEFAULTS
|
|
15
|
-
* 2. global settings: <agentDir>/settings.json
|
|
16
|
-
* 3. project settings: <cwd
|
|
22
|
+
* 2. global settings: <agentDir>/{settings.json,config.yml}
|
|
23
|
+
* 3. project settings: <cwd>/{.pi,.omp}/{settings.json,config.yml}
|
|
24
|
+
*
|
|
25
|
+
* Both host layouts are read (see ./host.ts): pi uses `.pi`, oh-my-pi uses
|
|
26
|
+
* `.omp`, and each file is keyed by the namespaced `llm-wiki` section.
|
|
17
27
|
*
|
|
18
28
|
* When `taskModel` is unset, the background lane falls back to the session
|
|
19
29
|
* model (see Runtime.resolveModel), so the feature is zero-config by default.
|
|
@@ -93,6 +103,27 @@ export interface TaskConfig {
|
|
|
93
103
|
*/
|
|
94
104
|
notices?: boolean;
|
|
95
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Let the PERSONAL wiki act as this project's ambient vault when the project
|
|
108
|
+
* has no wiki of its own.
|
|
109
|
+
*
|
|
110
|
+
* Ambient surfaces are the ones that fire without the user asking: the
|
|
111
|
+
* session notice, the periodic observe/retro reminder, and `before_agent_start`
|
|
112
|
+
* recall injection. `resolveVaultRoot` falls back to the personal vault when
|
|
113
|
+
* a project has none, so with this on those surfaces speak up in EVERY
|
|
114
|
+
* directory once a personal vault exists — injecting reminders and unrelated
|
|
115
|
+
* cross-project recall hits into repositories where no wiki was initialized.
|
|
116
|
+
*
|
|
117
|
+
* Host-dependent default, because the two hosts disagree on what silence
|
|
118
|
+
* means for a globally installed plugin:
|
|
119
|
+
* - pi → `true` (historical behavior, unchanged)
|
|
120
|
+
* - omp → `false` (a repository without a wiki stays quiet)
|
|
121
|
+
*
|
|
122
|
+
* The wiki TOOLS are registered either way, so `/wiki-init` and
|
|
123
|
+
* `wiki_bootstrap` always work; only the unprompted injections are gated.
|
|
124
|
+
*/
|
|
125
|
+
ambientPersonalVault?: boolean;
|
|
126
|
+
|
|
96
127
|
/**
|
|
97
128
|
* Agent-trajectory working-memory (capture → distill → recall), issue #80.
|
|
98
129
|
* OPT-IN, default OFF: only an explicit `trajectories: true` enables it.
|
|
@@ -120,6 +151,19 @@ export function noticesEnabled(config: TaskConfig | undefined): boolean {
|
|
|
120
151
|
return config?.notices !== false;
|
|
121
152
|
}
|
|
122
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Resolve whether the personal vault may serve as this project's ambient
|
|
156
|
+
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
157
|
+
* (see the field docs on {@link TaskConfig.ambientPersonalVault}).
|
|
158
|
+
*/
|
|
159
|
+
export function personalVaultIsAmbient(
|
|
160
|
+
config: TaskConfig | undefined,
|
|
161
|
+
host: HostKind = detectHost(),
|
|
162
|
+
): boolean {
|
|
163
|
+
if (typeof config?.ambientPersonalVault === "boolean") return config.ambientPersonalVault;
|
|
164
|
+
return host === "pi";
|
|
165
|
+
}
|
|
166
|
+
|
|
123
167
|
/**
|
|
124
168
|
* Resolve whether agent-trajectory working-memory is enabled (issue #80).
|
|
125
169
|
* INVERSE polarity of `noticesEnabled`: defaults to `false`; only an explicit
|
|
@@ -180,6 +224,10 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
180
224
|
out.notices = section.notices;
|
|
181
225
|
}
|
|
182
226
|
|
|
227
|
+
if (typeof section.ambientPersonalVault === "boolean") {
|
|
228
|
+
out.ambientPersonalVault = section.ambientPersonalVault;
|
|
229
|
+
}
|
|
230
|
+
|
|
183
231
|
if (typeof section.trajectories === "boolean") {
|
|
184
232
|
out.trajectories = section.trajectories;
|
|
185
233
|
}
|
|
@@ -235,14 +283,20 @@ export function validateSynthesisLanguage(tag: string): string | undefined {
|
|
|
235
283
|
}
|
|
236
284
|
|
|
237
285
|
/**
|
|
238
|
-
* Read a settings
|
|
286
|
+
* Read a settings file as a plain object, or `{}` when it is absent or
|
|
239
287
|
* corrupt. Reads directly (no `existsSync` pre-check) so there is no
|
|
240
288
|
* check-then-use race: a missing file throws ENOENT, which the catch treats
|
|
241
289
|
* the same as an empty file.
|
|
290
|
+
*
|
|
291
|
+
* `config.yml` / `config.yaml` are parsed as YAML — that is the format oh-my-pi
|
|
292
|
+
* migrates its settings to. Everything else is JSON. JSON is a YAML subset, so
|
|
293
|
+
* the YAML parser also accepts a `.yml` file that actually holds JSON.
|
|
242
294
|
*/
|
|
243
295
|
function readSettingsObject(path: string): Record<string, unknown> {
|
|
244
296
|
try {
|
|
245
|
-
const
|
|
297
|
+
const text = readFileSync(path, "utf-8");
|
|
298
|
+
const parsed =
|
|
299
|
+
path.endsWith(".yml") || path.endsWith(".yaml") ? parseYaml(text) : JSON.parse(text);
|
|
246
300
|
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
247
301
|
} catch {
|
|
248
302
|
// Missing or corrupt settings file: start from an empty object.
|
|
@@ -251,30 +305,25 @@ function readSettingsObject(path: string): Record<string, unknown> {
|
|
|
251
305
|
}
|
|
252
306
|
|
|
253
307
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
308
|
+
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
309
|
+
* other top-level key and every other setting in the section.
|
|
310
|
+
*
|
|
311
|
+
* The target file is chosen by `resolveProjectSettingsPath` — `.pi/settings.json`
|
|
312
|
+
* or `.omp/settings.json` depending on host and on what already exists — and is
|
|
313
|
+
* always JSON, which both hosts read.
|
|
260
314
|
*/
|
|
261
|
-
|
|
315
|
+
function updateProjectSection(
|
|
262
316
|
cwd: string,
|
|
263
|
-
|
|
317
|
+
mutate: (section: Record<string, unknown>) => void,
|
|
264
318
|
): void {
|
|
265
|
-
const settingsPath =
|
|
319
|
+
const settingsPath = resolveProjectSettingsPath(cwd);
|
|
266
320
|
const raw = readSettingsObject(settingsPath);
|
|
267
321
|
|
|
268
322
|
const existing = raw[SETTINGS_KEY];
|
|
269
323
|
const section: Record<string, unknown> =
|
|
270
324
|
existing && typeof existing === "object" ? { ...(existing as Record<string, unknown>) } : {};
|
|
271
325
|
|
|
272
|
-
|
|
273
|
-
section.taskModel = { provider: model.provider, id: model.id };
|
|
274
|
-
} else {
|
|
275
|
-
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
276
|
-
delete section.taskModel;
|
|
277
|
-
}
|
|
326
|
+
mutate(section);
|
|
278
327
|
raw[SETTINGS_KEY] = section;
|
|
279
328
|
|
|
280
329
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
@@ -282,44 +331,55 @@ export function persistTaskModel(
|
|
|
282
331
|
}
|
|
283
332
|
|
|
284
333
|
/**
|
|
285
|
-
* Persist the
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
334
|
+
* Persist (or clear) the wiki background `taskModel` in the PROJECT settings
|
|
335
|
+
* file under the namespaced `llm-wiki` key (issue #69). Project settings win
|
|
336
|
+
* over global in `loadTaskConfig`, so this takes effect immediately on the next
|
|
337
|
+
* config load. Passing `undefined` removes the key (reverting to the session
|
|
338
|
+
* model).
|
|
290
339
|
*/
|
|
291
|
-
export function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
raw[SETTINGS_KEY] = section;
|
|
340
|
+
export function persistTaskModel(
|
|
341
|
+
cwd: string,
|
|
342
|
+
model: { provider: string; id: string } | undefined,
|
|
343
|
+
): void {
|
|
344
|
+
updateProjectSection(cwd, (section) => {
|
|
345
|
+
if (model) {
|
|
346
|
+
section.taskModel = { provider: model.provider, id: model.id };
|
|
347
|
+
} else {
|
|
348
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
349
|
+
delete section.taskModel;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
306
353
|
|
|
307
|
-
|
|
308
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Persist the agent-trajectory flag in the PROJECT settings file under the
|
|
356
|
+
* namespaced `llm-wiki` key (issue #80). Mirrors `persistTaskModel`: `true`
|
|
357
|
+
* writes `trajectories: true`; `false` removes the key (reverting to the
|
|
358
|
+
* default-off behavior).
|
|
359
|
+
*/
|
|
360
|
+
export function persistTrajectoriesEnabled(cwd: string, enabled: boolean): void {
|
|
361
|
+
updateProjectSection(cwd, (section) => {
|
|
362
|
+
if (enabled) {
|
|
363
|
+
section.trajectories = true;
|
|
364
|
+
} else {
|
|
365
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
|
|
366
|
+
delete section.trajectories;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
309
369
|
}
|
|
310
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Merge the `llm-wiki` section from every settings file both hosts may use,
|
|
373
|
+
* lowest precedence first: built-in defaults, then user-level files, then
|
|
374
|
+
* project-level files. Absent files contribute nothing.
|
|
375
|
+
*/
|
|
311
376
|
export function loadTaskConfig(cwd: string): TaskConfig {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
} catch {
|
|
316
|
-
globalPath = "";
|
|
377
|
+
const config: TaskConfig = { ...TASK_DEFAULTS };
|
|
378
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
379
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
317
380
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
...(globalPath ? readNamespacedConfig(globalPath) : {}),
|
|
323
|
-
...readNamespacedConfig(projectPath),
|
|
324
|
-
};
|
|
381
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
382
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
383
|
+
}
|
|
384
|
+
return config;
|
|
325
385
|
}
|
|
@@ -821,7 +821,7 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
821
821
|
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
822
822
|
label: `lint:${paths.root}`,
|
|
823
823
|
started:
|
|
824
|
-
"\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted
|
|
824
|
+
"\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted with your next message.",
|
|
825
825
|
work: async () => runWikiLint(paths, params.auto_fix === true),
|
|
826
826
|
});
|
|
827
827
|
},
|
|
@@ -962,11 +962,14 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
962
962
|
"",
|
|
963
963
|
].filter(Boolean);
|
|
964
964
|
const reportPath = autoFix ? join(paths.outputs, `lint-${fmtDate()}.md`) : undefined;
|
|
965
|
+
// The gap snapshot is generated discovery metadata consumed by wiki_status:
|
|
966
|
+
// persist it on every successful lint so status never reports a stale count.
|
|
967
|
+
// Corrective actions below (report, event, meta rebuild) stay autoFix-only.
|
|
968
|
+
writeJson(join(paths.discoveries, "gaps.json"), {
|
|
969
|
+
gaps,
|
|
970
|
+
generated: new Date().toISOString(),
|
|
971
|
+
});
|
|
965
972
|
if (autoFix && reportPath) {
|
|
966
|
-
writeJson(join(paths.discoveries, "gaps.json"), {
|
|
967
|
-
gaps,
|
|
968
|
-
generated: new Date().toISOString(),
|
|
969
|
-
});
|
|
970
973
|
mkdirSync(paths.outputs, { recursive: true });
|
|
971
974
|
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf8");
|
|
972
975
|
appendEvent(paths, {
|
|
@@ -1106,7 +1109,7 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
|
|
|
1106
1109
|
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
1107
1110
|
label: `rebuild_meta:${paths.root}`,
|
|
1108
1111
|
started:
|
|
1109
|
-
"\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported
|
|
1112
|
+
"\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported with your next message.",
|
|
1110
1113
|
work: async () => {
|
|
1111
1114
|
const result = rebuildMetadata(paths);
|
|
1112
1115
|
// No rebuild_meta event — rebuild is a projection, not an authoritative mutation
|
|
@@ -1187,7 +1190,7 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
|
|
|
1187
1190
|
// report the stats on completion (issue #77).
|
|
1188
1191
|
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
1189
1192
|
label: `reindex_embeddings:${paths.root}`,
|
|
1190
|
-
started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported
|
|
1193
|
+
started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported with your next message.`,
|
|
1191
1194
|
details: { enabled: true, model: embedder.model },
|
|
1192
1195
|
work: async () => {
|
|
1193
1196
|
const stats = await reindexEmbeddings(paths, embedder, { force: params.force === true });
|
|
@@ -120,21 +120,45 @@ export function migrateDoubledPersonalVault(
|
|
|
120
120
|
/**
|
|
121
121
|
* Check if a vault is the personal wiki location.
|
|
122
122
|
* Used in layered recall to avoid double-counting.
|
|
123
|
+
*
|
|
124
|
+
* Compares PHYSICAL paths, not strings. On image-based ("atomic") Linux
|
|
125
|
+
* distributions `/home` is a symlink to `var/home`, so `homedir()` yields the
|
|
126
|
+
* `$HOME` string (`/home/u`) while `process.cwd()` — and therefore the root
|
|
127
|
+
* `resolveVaultRoot()` walks up to — yields `/var/home/u`. A string compare
|
|
128
|
+
* calls the personal vault a project vault, which makes layered recall search
|
|
129
|
+
* the same vault twice and `vaultPageCount()` double-count it.
|
|
130
|
+
*
|
|
131
|
+
* Exact equality, NOT containment: a vault nested under the home directory
|
|
132
|
+
* (`~/projects/foo/.llm-wiki`) is a project vault and must stay one.
|
|
123
133
|
*/
|
|
124
134
|
export function isPersonalVault(paths: VaultPaths): boolean {
|
|
125
|
-
|
|
135
|
+
const personalRoot = getPersonalWikiRoot();
|
|
136
|
+
// Fast path: identical strings need no filesystem syscalls.
|
|
137
|
+
if (paths.root === personalRoot) return true;
|
|
138
|
+
try {
|
|
139
|
+
return relativePhysicalPath(personalRoot, paths.root) === "";
|
|
140
|
+
} catch {
|
|
141
|
+
// Unresolvable path (permissions, symlink cycle): fall back to "not
|
|
142
|
+
// personal" so layered recall degrades to searching both vaults rather
|
|
143
|
+
// than silently dropping the personal layer.
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/**
|
|
129
|
-
* Resolve vault root
|
|
149
|
+
* Resolve the vault root that belongs to THIS project, or `null` when the
|
|
150
|
+
* project has none.
|
|
130
151
|
*
|
|
131
152
|
* Priority:
|
|
132
|
-
* 1. cwd has
|
|
133
|
-
* 2.
|
|
134
|
-
* 3.
|
|
135
|
-
*
|
|
153
|
+
* 1. cwd has `.llm-wiki/` (or legacy `.wiki/`) → project wiki (explicit)
|
|
154
|
+
* 2. `WIKI_HOME` → user-selected root, explicit enough to count as the project's
|
|
155
|
+
* 3. Walk up from cwd → parent project wiki (monorepo / nested workspace)
|
|
156
|
+
*
|
|
157
|
+
* Deliberately does NOT fall back to the personal wiki: callers that need the
|
|
158
|
+
* fallback use {@link resolveVaultRoot}, callers that must distinguish "this
|
|
159
|
+
* project has a wiki" from "some wiki exists somewhere" use this.
|
|
136
160
|
*/
|
|
137
|
-
export function
|
|
161
|
+
export function resolveProjectVaultRoot(cwd: string): string | null {
|
|
138
162
|
// A vault rooted at cwd is always the project-local choice.
|
|
139
163
|
if (detectVaultFormat(cwd) !== "none") return cwd;
|
|
140
164
|
|
|
@@ -142,19 +166,36 @@ export function resolveVaultRoot(cwd: string): string {
|
|
|
142
166
|
// over an unrelated personal vault found while walking parent directories.
|
|
143
167
|
if (process.env.WIKI_HOME) return process.env.WIKI_HOME;
|
|
144
168
|
|
|
145
|
-
// Walk up looking for a vault sentinel (new or legacy)
|
|
169
|
+
// Walk up looking for a vault sentinel (new or legacy).
|
|
146
170
|
let dir = cwd;
|
|
147
171
|
while (dir !== dirname(dir)) {
|
|
148
172
|
dir = dirname(dir);
|
|
149
|
-
if (detectVaultFormat(dir)
|
|
173
|
+
if (detectVaultFormat(dir) === "none") continue;
|
|
174
|
+
// Skip the personal vault: it is an ancestor of EVERY project under the
|
|
175
|
+
// home directory (`~/projects/foo`, and on Windows even the temp dir), so
|
|
176
|
+
// counting it here would report a project vault for directories that have
|
|
177
|
+
// none. `resolveVaultRoot` still falls back to it explicitly.
|
|
178
|
+
if (isPersonalVault(getVaultPaths(dir))) continue;
|
|
179
|
+
return dir;
|
|
150
180
|
}
|
|
151
181
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (detectVaultFormat(personalRoot) !== "none") return personalRoot;
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
155
184
|
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Resolve vault root from cwd with personal fallback.
|
|
187
|
+
*
|
|
188
|
+
* Priority:
|
|
189
|
+
* 1-3. {@link resolveProjectVaultRoot}
|
|
190
|
+
* 4. Personal wiki root (`~`, or `WIKI_HOME`) — used whether or not it already
|
|
191
|
+
* holds a vault, so first-run bootstrap has somewhere to write.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveVaultRoot(cwd: string): string {
|
|
194
|
+
// Realpath the personal fallback so a symlinked `$HOME` (atomic-OS layouts)
|
|
195
|
+
// yields the PHYSICAL root the ancestor walk used to return — the #145
|
|
196
|
+
// regression guard pins that. `realpathWithMissingTail` also covers first-run
|
|
197
|
+
// bootstrap, where the personal root does not exist on disk yet.
|
|
198
|
+
return resolveProjectVaultRoot(cwd) ?? realpathWithMissingTail(getPersonalWikiRoot());
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
/** Get all vault paths for the new (.llm-wiki) layout. */
|
package/mcp/index.ts
CHANGED
|
@@ -15,9 +15,10 @@ import { join } from "node:path";
|
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
16
16
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
17
17
|
import * as z from "zod/v4";
|
|
18
|
-
import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
18
|
+
import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
19
19
|
import { createExecApi } from "./exec.js";
|
|
20
20
|
import {
|
|
21
|
+
bootstrapOperation,
|
|
21
22
|
captureSourceOperation,
|
|
22
23
|
recallOperation,
|
|
23
24
|
retroOperation,
|
|
@@ -35,6 +36,19 @@ function getPaths(): ReturnType<typeof resolveVaultPaths> {
|
|
|
35
36
|
return resolveVaultPaths(root);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The vault root this server was configured with, without resolution.
|
|
41
|
+
*
|
|
42
|
+
* `getPaths()` RESOLVES an existing vault: on a root that has none it walks up
|
|
43
|
+
* to a parent vault and then falls back to the personal vault. That is right
|
|
44
|
+
* for reading and writing pages, and wrong for creating one — bootstrap must
|
|
45
|
+
* create the vault where the client pointed the server, not wherever
|
|
46
|
+
* resolution lands. The Pi tool draws the same distinction.
|
|
47
|
+
*/
|
|
48
|
+
function getConfiguredPaths(): ReturnType<typeof getVaultPaths> {
|
|
49
|
+
return getVaultPaths(process.env.WIKI_ROOT || process.cwd());
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
function hasVault(): boolean {
|
|
39
53
|
const paths = getPaths();
|
|
40
54
|
return existsSync(join(paths.dotWiki, "config.json"));
|
|
@@ -47,13 +61,62 @@ const server = new McpServer({
|
|
|
47
61
|
version: "1.0.0",
|
|
48
62
|
});
|
|
49
63
|
|
|
64
|
+
// ---- wiki_bootstrap ----
|
|
65
|
+
//
|
|
66
|
+
// Registered first, and the only tool not gated on an existing vault: the
|
|
67
|
+
// other five fail closed with a message naming this one, which an MCP-only
|
|
68
|
+
// client could not act on while it was extension-only (issue #130).
|
|
69
|
+
|
|
70
|
+
server.registerTool(
|
|
71
|
+
"wiki_bootstrap",
|
|
72
|
+
{
|
|
73
|
+
description:
|
|
74
|
+
"Create an LLM Wiki vault at this server's wiki root (WIKI_ROOT, or the working directory). Writes config, schema, templates and metadata scaffolding. Run this first when no vault exists; safe to re-run on an existing vault, where it updates the config and rebuilds metadata without touching pages.",
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
topic: z.string().describe("Main topic of the wiki"),
|
|
77
|
+
mode: z.string().optional().describe("personal or company (default: personal)"),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
async ({ topic, mode }) => {
|
|
81
|
+
const paths = getConfiguredPaths();
|
|
82
|
+
const result = await bootstrapOperation(paths, { topic, mode });
|
|
83
|
+
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text" as const,
|
|
89
|
+
text: `Vault error: ${result.diagnostics[0].message}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const warnings = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text" as const,
|
|
101
|
+
text: [
|
|
102
|
+
`${result.created ? "Wiki vault created" : "Wiki vault updated"} at ${paths.root}`,
|
|
103
|
+
"Structure: .llm-wiki/{raw,wiki,meta} plus config and WIKI_SCHEMA.md",
|
|
104
|
+
"Next: capture a source with wiki_capture_source, or save an insight with wiki_retro.",
|
|
105
|
+
...warnings,
|
|
106
|
+
].join("\n"),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
50
113
|
// ---- wiki_recall ----
|
|
51
114
|
|
|
52
115
|
server.registerTool(
|
|
53
116
|
"wiki_recall",
|
|
54
117
|
{
|
|
55
118
|
description:
|
|
56
|
-
"Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
|
|
119
|
+
"Search the wiki for pages relevant to a query. Searches the resolved vault and the personal vault (~/.llm-wiki) together, deduplicated, with personal hits labelled. Returns matching page IDs, titles, types, and content previews.",
|
|
57
120
|
inputSchema: z.object({
|
|
58
121
|
query: z.string().describe("Search query — use the user's full request or key terms"),
|
|
59
122
|
max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
|
package/mcp/operations.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
|
|
10
11
|
import { type ProjectionResult, rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
|
|
11
|
-
import {
|
|
12
|
+
import { type RecallResult, searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
|
|
12
13
|
import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
|
|
13
14
|
import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
|
|
14
15
|
import type { VaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
@@ -30,16 +31,58 @@ function projectionOutcome(
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Shared bootstrap operation: create (or update) the vault at `paths`.
|
|
36
|
+
*
|
|
37
|
+
* This is the one operation that must work when no vault exists — every other
|
|
38
|
+
* one fails closed naming it. `bootstrapVault` is pure Node (`node:fs`,
|
|
39
|
+
* `node:path` and sibling lib modules), so it needs no model and no
|
|
40
|
+
* credentials, which is what makes it fit the MCP surface.
|
|
41
|
+
*
|
|
42
|
+
* A failed projection rebuild is reported as diagnostics alongside `ok: true`:
|
|
43
|
+
* the vault has been written to disk by then, and `wiki_lint` is the repair
|
|
44
|
+
* path, so failing the call outright would misreport what happened.
|
|
45
|
+
*/
|
|
46
|
+
export async function bootstrapOperation(
|
|
47
|
+
paths: VaultPaths,
|
|
48
|
+
input: { topic: string; mode?: string },
|
|
49
|
+
): Promise<
|
|
50
|
+
| { ok: true; created: boolean; diagnostics: Array<{ code: string; message: string }> }
|
|
51
|
+
| { ok: false; diagnostics: Array<{ code: string; message: string }> }
|
|
52
|
+
> {
|
|
53
|
+
const result = bootstrapVault(paths, { topic: input.topic, mode: input.mode ?? "personal" });
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
diagnostics: result.diagnostics.map(({ code, message }) => ({ code, message })),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const projection = projectionOutcome(result.projection);
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
created: result.created,
|
|
64
|
+
diagnostics: projection.ok ? [] : projection.diagnostics,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Shared recall operation: layered search plus vault diagnostics.
|
|
70
|
+
*
|
|
71
|
+
* Layering is the shared contract, not an extension-only feature: MCP clients
|
|
72
|
+
* get the same personal + project merge the Pi `wiki_recall` tool does.
|
|
73
|
+
* `searchWikiLayered` appends personal-vault hits, deduplicates by page ID and
|
|
74
|
+
* tags personal results with `vaultLabel`. It is a no-op when no personal vault
|
|
75
|
+
* exists, or when the resolved vault IS the personal vault.
|
|
76
|
+
*/
|
|
34
77
|
export async function recallOperation(
|
|
35
78
|
paths: VaultPaths,
|
|
36
79
|
query: string,
|
|
37
80
|
maxResults = 5,
|
|
38
81
|
): Promise<{
|
|
39
|
-
results:
|
|
82
|
+
results: RecallResult[];
|
|
40
83
|
diagnostics: Array<{ code: string; message: string }>;
|
|
41
84
|
}> {
|
|
42
|
-
const results =
|
|
85
|
+
const results = searchWikiLayered(paths, query, maxResults);
|
|
43
86
|
const vaultState = inspectVaultFormat(paths);
|
|
44
87
|
return {
|
|
45
88
|
results,
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
9
|
"pi-skill",
|
|
10
|
+
"omp",
|
|
11
|
+
"oh-my-pi",
|
|
12
|
+
"omp-plugin",
|
|
13
|
+
"omp-extension",
|
|
10
14
|
"llm-wiki",
|
|
11
15
|
"karpathy",
|
|
12
16
|
"knowledge-base",
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
"extensions",
|
|
38
42
|
"skills",
|
|
39
43
|
"prompts",
|
|
44
|
+
"commands",
|
|
40
45
|
"scripts/migrate-llm-wiki.js",
|
|
41
46
|
"mcp",
|
|
42
47
|
"dist",
|
|
@@ -64,6 +69,11 @@
|
|
|
64
69
|
"llm-wiki": "node ./dist/mcp/index.js"
|
|
65
70
|
}
|
|
66
71
|
},
|
|
72
|
+
"omp": {
|
|
73
|
+
"extensions": [
|
|
74
|
+
"./extensions/llm-wiki/index.ts"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
67
77
|
"peerDependencies": {
|
|
68
78
|
"@mariozechner/pi-coding-agent": "*",
|
|
69
79
|
"typebox": "*"
|
|
@@ -96,6 +106,7 @@
|
|
|
96
106
|
"test": "vitest run",
|
|
97
107
|
"test:watch": "vitest",
|
|
98
108
|
"test:coverage": "vitest run --coverage",
|
|
109
|
+
"build:commands": "node scripts/build-commands.js",
|
|
99
110
|
"build:mcp": "node scripts/build-mcp.js",
|
|
100
111
|
"typecheck": "tsc --noEmit",
|
|
101
112
|
"lint": "biome check .",
|