@zosmaai/pi-llm-wiki 0.11.3 → 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 +10 -0
- package/README.md +80 -2
- 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/task-config.js +74 -49
- package/dist/extensions/llm-wiki/lib/utils.js +59 -16
- 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/task-config.ts +115 -55
- package/extensions/llm-wiki/lib/utils.ts +55 -14
- 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
|
}
|
|
@@ -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/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 .",
|