@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,7 @@
|
|
|
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 { detectHost, listGlobalSettingsFiles, listProjectSettingsFiles, resolveProjectSettingsPath, } from "./host.js";
|
|
4
5
|
export const TASK_DEFAULTS = {};
|
|
5
6
|
/**
|
|
6
7
|
* Resolve whether user-facing wiki notices are enabled (issue #77). Defaults
|
|
@@ -9,6 +10,16 @@ export const TASK_DEFAULTS = {};
|
|
|
9
10
|
export function noticesEnabled(config) {
|
|
10
11
|
return config?.notices !== false;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve whether the personal vault may serve as this project's ambient
|
|
15
|
+
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
16
|
+
* (see the field docs on {@link TaskConfig.ambientPersonalVault}).
|
|
17
|
+
*/
|
|
18
|
+
export function personalVaultIsAmbient(config, host = detectHost()) {
|
|
19
|
+
if (typeof config?.ambientPersonalVault === "boolean")
|
|
20
|
+
return config.ambientPersonalVault;
|
|
21
|
+
return host === "pi";
|
|
22
|
+
}
|
|
12
23
|
/**
|
|
13
24
|
* Resolve whether agent-trajectory working-memory is enabled (issue #80).
|
|
14
25
|
* INVERSE polarity of `noticesEnabled`: defaults to `false`; only an explicit
|
|
@@ -64,6 +75,9 @@ function readNamespacedConfig(path) {
|
|
|
64
75
|
if (typeof section.notices === "boolean") {
|
|
65
76
|
out.notices = section.notices;
|
|
66
77
|
}
|
|
78
|
+
if (typeof section.ambientPersonalVault === "boolean") {
|
|
79
|
+
out.ambientPersonalVault = section.ambientPersonalVault;
|
|
80
|
+
}
|
|
67
81
|
if (typeof section.trajectories === "boolean") {
|
|
68
82
|
out.trajectories = section.trajectories;
|
|
69
83
|
}
|
|
@@ -121,14 +135,19 @@ export function validateSynthesisLanguage(tag) {
|
|
|
121
135
|
return canonical[0];
|
|
122
136
|
}
|
|
123
137
|
/**
|
|
124
|
-
* Read a settings
|
|
138
|
+
* Read a settings file as a plain object, or `{}` when it is absent or
|
|
125
139
|
* corrupt. Reads directly (no `existsSync` pre-check) so there is no
|
|
126
140
|
* check-then-use race: a missing file throws ENOENT, which the catch treats
|
|
127
141
|
* the same as an empty file.
|
|
142
|
+
*
|
|
143
|
+
* `config.yml` / `config.yaml` are parsed as YAML — that is the format oh-my-pi
|
|
144
|
+
* migrates its settings to. Everything else is JSON. JSON is a YAML subset, so
|
|
145
|
+
* the YAML parser also accepts a `.yml` file that actually holds JSON.
|
|
128
146
|
*/
|
|
129
147
|
function readSettingsObject(path) {
|
|
130
148
|
try {
|
|
131
|
-
const
|
|
149
|
+
const text = readFileSync(path, "utf-8");
|
|
150
|
+
const parsed = path.endsWith(".yml") || path.endsWith(".yaml") ? parseYaml(text) : JSON.parse(text);
|
|
132
151
|
if (parsed && typeof parsed === "object")
|
|
133
152
|
return parsed;
|
|
134
153
|
}
|
|
@@ -138,64 +157,70 @@ function readSettingsObject(path) {
|
|
|
138
157
|
return {};
|
|
139
158
|
}
|
|
140
159
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
160
|
+
* Rewrite the `llm-wiki` section of the project settings file, preserving every
|
|
161
|
+
* other top-level key and every other setting in the section.
|
|
162
|
+
*
|
|
163
|
+
* The target file is chosen by `resolveProjectSettingsPath` — `.pi/settings.json`
|
|
164
|
+
* or `.omp/settings.json` depending on host and on what already exists — and is
|
|
165
|
+
* always JSON, which both hosts read.
|
|
147
166
|
*/
|
|
148
|
-
|
|
149
|
-
const settingsPath =
|
|
167
|
+
function updateProjectSection(cwd, mutate) {
|
|
168
|
+
const settingsPath = resolveProjectSettingsPath(cwd);
|
|
150
169
|
const raw = readSettingsObject(settingsPath);
|
|
151
170
|
const existing = raw[SETTINGS_KEY];
|
|
152
171
|
const section = existing && typeof existing === "object" ? { ...existing } : {};
|
|
153
|
-
|
|
154
|
-
section.taskModel = { provider: model.provider, id: model.id };
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
158
|
-
delete section.taskModel;
|
|
159
|
-
}
|
|
172
|
+
mutate(section);
|
|
160
173
|
raw[SETTINGS_KEY] = section;
|
|
161
174
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
162
175
|
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
163
176
|
}
|
|
164
177
|
/**
|
|
165
|
-
* Persist the
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
178
|
+
* Persist (or clear) the wiki background `taskModel` in the PROJECT settings
|
|
179
|
+
* file under the namespaced `llm-wiki` key (issue #69). Project settings win
|
|
180
|
+
* over global in `loadTaskConfig`, so this takes effect immediately on the next
|
|
181
|
+
* config load. Passing `undefined` removes the key (reverting to the session
|
|
182
|
+
* model).
|
|
183
|
+
*/
|
|
184
|
+
export function persistTaskModel(cwd, model) {
|
|
185
|
+
updateProjectSection(cwd, (section) => {
|
|
186
|
+
if (model) {
|
|
187
|
+
section.taskModel = { provider: model.provider, id: model.id };
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
|
|
191
|
+
delete section.taskModel;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Persist the agent-trajectory flag in the PROJECT settings file under the
|
|
197
|
+
* namespaced `llm-wiki` key (issue #80). Mirrors `persistTaskModel`: `true`
|
|
198
|
+
* writes `trajectories: true`; `false` removes the key (reverting to the
|
|
199
|
+
* default-off behavior).
|
|
170
200
|
*/
|
|
171
201
|
export function persistTrajectoriesEnabled(cwd, enabled) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
delete section.trajectories;
|
|
182
|
-
}
|
|
183
|
-
raw[SETTINGS_KEY] = section;
|
|
184
|
-
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
185
|
-
writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
202
|
+
updateProjectSection(cwd, (section) => {
|
|
203
|
+
if (enabled) {
|
|
204
|
+
section.trajectories = true;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
|
|
208
|
+
delete section.trajectories;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
186
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Merge the `llm-wiki` section from every settings file both hosts may use,
|
|
214
|
+
* lowest precedence first: built-in defaults, then user-level files, then
|
|
215
|
+
* project-level files. Absent files contribute nothing.
|
|
216
|
+
*/
|
|
187
217
|
export function loadTaskConfig(cwd) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
218
|
+
const config = { ...TASK_DEFAULTS };
|
|
219
|
+
for (const path of listGlobalSettingsFiles()) {
|
|
220
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
191
221
|
}
|
|
192
|
-
|
|
193
|
-
|
|
222
|
+
for (const path of listProjectSettingsFiles(cwd)) {
|
|
223
|
+
Object.assign(config, readNamespacedConfig(path));
|
|
194
224
|
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
...TASK_DEFAULTS,
|
|
198
|
-
...(globalPath ? readNamespacedConfig(globalPath) : {}),
|
|
199
|
-
...readNamespacedConfig(projectPath),
|
|
200
|
-
};
|
|
225
|
+
return config;
|
|
201
226
|
}
|
|
@@ -82,20 +82,46 @@ export function migrateDoubledPersonalVault(parentRoot = getPersonalWikiRoot())
|
|
|
82
82
|
/**
|
|
83
83
|
* Check if a vault is the personal wiki location.
|
|
84
84
|
* Used in layered recall to avoid double-counting.
|
|
85
|
+
*
|
|
86
|
+
* Compares PHYSICAL paths, not strings. On image-based ("atomic") Linux
|
|
87
|
+
* distributions `/home` is a symlink to `var/home`, so `homedir()` yields the
|
|
88
|
+
* `$HOME` string (`/home/u`) while `process.cwd()` — and therefore the root
|
|
89
|
+
* `resolveVaultRoot()` walks up to — yields `/var/home/u`. A string compare
|
|
90
|
+
* calls the personal vault a project vault, which makes layered recall search
|
|
91
|
+
* the same vault twice and `vaultPageCount()` double-count it.
|
|
92
|
+
*
|
|
93
|
+
* Exact equality, NOT containment: a vault nested under the home directory
|
|
94
|
+
* (`~/projects/foo/.llm-wiki`) is a project vault and must stay one.
|
|
85
95
|
*/
|
|
86
96
|
export function isPersonalVault(paths) {
|
|
87
|
-
|
|
97
|
+
const personalRoot = getPersonalWikiRoot();
|
|
98
|
+
// Fast path: identical strings need no filesystem syscalls.
|
|
99
|
+
if (paths.root === personalRoot)
|
|
100
|
+
return true;
|
|
101
|
+
try {
|
|
102
|
+
return relativePhysicalPath(personalRoot, paths.root) === "";
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Unresolvable path (permissions, symlink cycle): fall back to "not
|
|
106
|
+
// personal" so layered recall degrades to searching both vaults rather
|
|
107
|
+
// than silently dropping the personal layer.
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
88
110
|
}
|
|
89
111
|
/**
|
|
90
|
-
* Resolve vault root
|
|
112
|
+
* Resolve the vault root that belongs to THIS project, or `null` when the
|
|
113
|
+
* project has none.
|
|
91
114
|
*
|
|
92
115
|
* Priority:
|
|
93
|
-
* 1. cwd has
|
|
94
|
-
* 2.
|
|
95
|
-
* 3.
|
|
96
|
-
*
|
|
116
|
+
* 1. cwd has `.llm-wiki/` (or legacy `.wiki/`) → project wiki (explicit)
|
|
117
|
+
* 2. `WIKI_HOME` → user-selected root, explicit enough to count as the project's
|
|
118
|
+
* 3. Walk up from cwd → parent project wiki (monorepo / nested workspace)
|
|
119
|
+
*
|
|
120
|
+
* Deliberately does NOT fall back to the personal wiki: callers that need the
|
|
121
|
+
* fallback use {@link resolveVaultRoot}, callers that must distinguish "this
|
|
122
|
+
* project has a wiki" from "some wiki exists somewhere" use this.
|
|
97
123
|
*/
|
|
98
|
-
export function
|
|
124
|
+
export function resolveProjectVaultRoot(cwd) {
|
|
99
125
|
// A vault rooted at cwd is always the project-local choice.
|
|
100
126
|
if (detectVaultFormat(cwd) !== "none")
|
|
101
127
|
return cwd;
|
|
@@ -103,19 +129,36 @@ export function resolveVaultRoot(cwd) {
|
|
|
103
129
|
// over an unrelated personal vault found while walking parent directories.
|
|
104
130
|
if (process.env.WIKI_HOME)
|
|
105
131
|
return process.env.WIKI_HOME;
|
|
106
|
-
// Walk up looking for a vault sentinel (new or legacy)
|
|
132
|
+
// Walk up looking for a vault sentinel (new or legacy).
|
|
107
133
|
let dir = cwd;
|
|
108
134
|
while (dir !== dirname(dir)) {
|
|
109
135
|
dir = dirname(dir);
|
|
110
|
-
if (detectVaultFormat(dir)
|
|
111
|
-
|
|
136
|
+
if (detectVaultFormat(dir) === "none")
|
|
137
|
+
continue;
|
|
138
|
+
// Skip the personal vault: it is an ancestor of EVERY project under the
|
|
139
|
+
// home directory (`~/projects/foo`, and on Windows even the temp dir), so
|
|
140
|
+
// counting it here would report a project vault for directories that have
|
|
141
|
+
// none. `resolveVaultRoot` still falls back to it explicitly.
|
|
142
|
+
if (isPersonalVault(getVaultPaths(dir)))
|
|
143
|
+
continue;
|
|
144
|
+
return dir;
|
|
112
145
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve vault root from cwd with personal fallback.
|
|
150
|
+
*
|
|
151
|
+
* Priority:
|
|
152
|
+
* 1-3. {@link resolveProjectVaultRoot}
|
|
153
|
+
* 4. Personal wiki root (`~`, or `WIKI_HOME`) — used whether or not it already
|
|
154
|
+
* holds a vault, so first-run bootstrap has somewhere to write.
|
|
155
|
+
*/
|
|
156
|
+
export function resolveVaultRoot(cwd) {
|
|
157
|
+
// Realpath the personal fallback so a symlinked `$HOME` (atomic-OS layouts)
|
|
158
|
+
// yields the PHYSICAL root the ancestor walk used to return — the #145
|
|
159
|
+
// regression guard pins that. `realpathWithMissingTail` also covers first-run
|
|
160
|
+
// bootstrap, where the personal root does not exist on disk yet.
|
|
161
|
+
return resolveProjectVaultRoot(cwd) ?? realpathWithMissingTail(getPersonalWikiRoot());
|
|
119
162
|
}
|
|
120
163
|
/** Get all vault paths for the new (.llm-wiki) layout. */
|
|
121
164
|
export function getVaultPaths(root) {
|
package/docs/configuration.md
CHANGED
|
@@ -31,17 +31,34 @@ The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always avail
|
|
|
31
31
|
| ----------------------------- | ----------- | ----------------------------------------------- |
|
|
32
32
|
| `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
|
|
33
33
|
| `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
|
|
34
|
+
| `LLM_WIKI_HOST` | auto | Force the host layout: `pi` or `omp` |
|
|
34
35
|
|
|
35
|
-
##
|
|
36
|
+
## Agent Settings
|
|
36
37
|
|
|
37
|
-
Runtime settings for the wiki's background tasks live
|
|
38
|
+
Runtime settings for the wiki's background tasks live under the `llm-wiki`
|
|
39
|
+
namespace of the host's settings file. Both host layouts are read and merged,
|
|
40
|
+
lowest precedence first:
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
1. `<agentDir>/settings.json`, then `config.yml` / `config.yaml`
|
|
43
|
+
— `~/.pi/agent` under pi, `~/.omp/agent` under oh-my-pi
|
|
44
|
+
2. `<cwd>/.pi/{settings.json,config.yml,config.yaml}`
|
|
45
|
+
3. `<cwd>/.omp/{settings.json,config.yml,config.yaml}`
|
|
46
|
+
|
|
47
|
+
The **host-native** project directory is applied last, so it wins: `.omp` under
|
|
48
|
+
oh-my-pi, `.pi` under pi. Reading the other host's directory means a vault
|
|
49
|
+
configured under pi keeps working after `omp` takes over the repository.
|
|
50
|
+
|
|
51
|
+
`/wiki-model` and `/wiki-trajectories` write JSON only, into whichever project
|
|
52
|
+
config directory already exists (host-native first, created if neither is
|
|
53
|
+
present). A hand-authored `config.yml` is read but never rewritten.
|
|
54
|
+
|
|
55
|
+
| Setting | Default | Description |
|
|
56
|
+
| ---------------------- | ---------- | ------------------------------------------------------------ |
|
|
57
|
+
| `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
|
|
58
|
+
| `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
|
|
59
|
+
| `trajectories` | false | Enable agent-trajectory working-memory |
|
|
60
|
+
| `notices` | true | Show wiki activity notices in chat |
|
|
61
|
+
| `ambientPersonalVault` | host-dependent | Let the personal vault act as the ambient vault in projects that have no wiki. `true` under pi, `false` under oh-my-pi — see below. |
|
|
45
62
|
|
|
46
63
|
Example:
|
|
47
64
|
|
|
@@ -66,6 +83,30 @@ The vault root is resolved in this priority order:
|
|
|
66
83
|
|
|
67
84
|
This means when you're in a project with its own `.llm-wiki/`, that project wiki is active. When you're outside any project wiki, your personal `~/.llm-wiki/` takes over automatically.
|
|
68
85
|
|
|
86
|
+
### Ambient surfaces in projects without a wiki
|
|
87
|
+
|
|
88
|
+
Three surfaces fire without being asked: the session notice, the periodic
|
|
89
|
+
observe/retro reminder, and the `before_agent_start` recall injection (plus its
|
|
90
|
+
`<wiki_status>` system-prompt footer).
|
|
91
|
+
|
|
92
|
+
Because vault resolution falls back to the personal vault, those surfaces would
|
|
93
|
+
otherwise speak up in *every* directory as soon as `~/.llm-wiki/` exists —
|
|
94
|
+
injecting reminders and unrelated cross-project recall hits into repositories
|
|
95
|
+
where no wiki was ever initialized. Under oh-my-pi the plugin is installed once
|
|
96
|
+
and loads in every project, so that fallback is **off** by default there; under
|
|
97
|
+
pi the historical behaviour is kept.
|
|
98
|
+
|
|
99
|
+
`ambientPersonalVault` overrides the host default in either direction:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{ "llm-wiki": { "ambientPersonalVault": true } }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The gate only affects unprompted injections. Tools and slash commands are
|
|
106
|
+
always registered, so `/wiki-init` and `wiki_bootstrap` work in any directory —
|
|
107
|
+
and once a project has its own `.llm-wiki/`, every ambient surface turns back on
|
|
108
|
+
for it.
|
|
109
|
+
|
|
69
110
|
## Page Frontmatter
|
|
70
111
|
|
|
71
112
|
```yaml
|
|
@@ -20,7 +20,12 @@ import {
|
|
|
20
20
|
} from "./lib/recall.js";
|
|
21
21
|
import { registerWikiRetro } from "./lib/retro.js";
|
|
22
22
|
import { registerBackgroundRuntime } from "./lib/runtime.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
loadTaskConfig,
|
|
25
|
+
noticesEnabled,
|
|
26
|
+
personalVaultIsAmbient,
|
|
27
|
+
trajectoriesEnabled,
|
|
28
|
+
} from "./lib/task-config.js";
|
|
24
29
|
import {
|
|
25
30
|
registerWikiBootstrap,
|
|
26
31
|
registerWikiCaptureSource,
|
|
@@ -40,7 +45,11 @@ import {
|
|
|
40
45
|
registerWikiDistillSkills,
|
|
41
46
|
registerWikiRecallSkill,
|
|
42
47
|
} from "./lib/trajectory.js";
|
|
43
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
migrateDoubledPersonalVault,
|
|
50
|
+
resolveProjectVaultRoot,
|
|
51
|
+
resolveVaultPaths,
|
|
52
|
+
} from "./lib/utils.js";
|
|
44
53
|
import { inspectWritableVault } from "./lib/vault-format.js";
|
|
45
54
|
import { applySessionStartStatus } from "./lib/visible-status.js";
|
|
46
55
|
|
|
@@ -68,6 +77,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
68
77
|
// work. Created first so tools (e.g. wiki_ingest) can dispatch to it.
|
|
69
78
|
const runtime = registerBackgroundRuntime(pi);
|
|
70
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Does a wiki apply to the directory this session is working in?
|
|
82
|
+
*
|
|
83
|
+
* Gates every AMBIENT surface — the ones that speak without being asked:
|
|
84
|
+
* session bootstrap/notice, the periodic observe/retro reminder, and
|
|
85
|
+
* `before_agent_start` recall injection. Tools and commands are registered
|
|
86
|
+
* regardless, so `/wiki-init` remains the way in.
|
|
87
|
+
*
|
|
88
|
+
* `resolveVaultRoot` falls back to the personal vault when a project has
|
|
89
|
+
* none, which is why the ambient surfaces used to fire in EVERY directory
|
|
90
|
+
* once a personal vault existed — reminders and unrelated cross-project
|
|
91
|
+
* recall hits leaking into repositories that never initialized a wiki.
|
|
92
|
+
* Under omp that fallback is off by default (`llm-wiki.ambientPersonalVault`);
|
|
93
|
+
* under pi it stays on, preserving the historical behavior.
|
|
94
|
+
*
|
|
95
|
+
* Resolved per call, not once at load: `cwd` changes within a session.
|
|
96
|
+
*/
|
|
97
|
+
const wikiAppliesTo = (cwd: string): boolean =>
|
|
98
|
+
resolveProjectVaultRoot(cwd) !== null || personalVaultIsAmbient(runtime.config);
|
|
99
|
+
|
|
71
100
|
registerWikiBootstrap(pi);
|
|
72
101
|
registerWikiCaptureSource(pi, runtime);
|
|
73
102
|
registerWikiIngest(pi, runtime);
|
|
@@ -106,9 +135,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
135
|
registerWikiObserve(pi, runtime, reminderState);
|
|
107
136
|
// Visible observe/retro reminder by default (issue #77); silenced when the
|
|
108
137
|
// user sets `llm-wiki.notices: false`. Resolver reads the live config so the
|
|
109
|
-
// setting takes effect without a restart.
|
|
138
|
+
// setting takes effect without a restart. `display: false` still injects the
|
|
139
|
+
// reminder into model context, so the "no wiki here" case needs its own gate.
|
|
110
140
|
registerObservationReminder(pi, reminderState, {
|
|
111
141
|
display: () => noticesEnabled(runtime.config),
|
|
142
|
+
enabled: () => wikiAppliesTo(process.cwd()),
|
|
112
143
|
});
|
|
113
144
|
|
|
114
145
|
installGuardrails(pi, runtime);
|
|
@@ -137,6 +168,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
137
168
|
console.warn(`[llm-wiki] doubled-dotdir migration skipped: ${(err as Error).message}`);
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
// Ambient gate. `ensureConfig` first so an explicit
|
|
172
|
+
// `llm-wiki.ambientPersonalVault` is honored on the very first session —
|
|
173
|
+
// `runtime.config` is otherwise empty until the first `turn_start`.
|
|
174
|
+
runtime.ensureConfig(process.cwd());
|
|
175
|
+
if (!wikiAppliesTo(process.cwd())) return;
|
|
176
|
+
|
|
140
177
|
const paths = resolveVaultPaths(process.cwd());
|
|
141
178
|
if (!existsSync(join(paths.dotWiki, "config.json"))) {
|
|
142
179
|
// Silently create the wiki vault — no UI prompts. Topic/mode will be
|
|
@@ -168,9 +205,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
168
205
|
|
|
169
206
|
// Surface the "wiki active" badge and the active background task model
|
|
170
207
|
// (issue #69), both gated by `llm-wiki.notices` (issue #77, regression
|
|
171
|
-
// fixed in #83, helper extracted in #84). `ensureConfig`
|
|
172
|
-
//
|
|
173
|
-
runtime.ensureConfig(process.cwd());
|
|
208
|
+
// fixed in #83, helper extracted in #84). The `ensureConfig` above the
|
|
209
|
+
// ambient gate already loaded the project settings this reads.
|
|
174
210
|
applySessionStartStatus({
|
|
175
211
|
ui: ctx.ui,
|
|
176
212
|
runtime,
|
|
@@ -196,6 +232,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
232
|
// from the user's first prompt and update config via wiki_bootstrap.
|
|
197
233
|
// 2. Search both personal + project vaults for relevant pages.
|
|
198
234
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
235
|
+
if (!wikiAppliesTo(process.cwd())) return;
|
|
236
|
+
|
|
199
237
|
const paths = resolveVaultPaths(process.cwd());
|
|
200
238
|
if (!existsSync(join(paths.dotWiki, "config.json"))) {
|
|
201
239
|
return;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Host adapter for the two coding agents that can load this extension:
|
|
7
|
+
*
|
|
8
|
+
* - **pi** — `@mariozechner/pi-coding-agent`, config dir `.pi`
|
|
9
|
+
* - **omp** — oh-my-pi (`@oh-my-pi/pi-coding-agent`), config dir `.omp`
|
|
10
|
+
*
|
|
11
|
+
* omp rewrites `@mariozechner/pi-*` (and bare `typebox`) imports onto its own
|
|
12
|
+
* bundled packages at load time (its `legacy-pi-compat.ts`), so the *module
|
|
13
|
+
* graph* needs no changes. What does differ is the on-disk config layout:
|
|
14
|
+
*
|
|
15
|
+
* | | pi | omp |
|
|
16
|
+
* |---|---|---|
|
|
17
|
+
* | user dir | `~/.pi/agent` | `~/.omp/agent` |
|
|
18
|
+
* | project dir | `<cwd>/.pi` | `<cwd>/.omp` |
|
|
19
|
+
* | settings file | `settings.json` | `settings.json`, then `config.yml` |
|
|
20
|
+
*
|
|
21
|
+
* omp explicitly does **not** read `.pi` (its config source order is
|
|
22
|
+
* `.omp` → `.claude` → `.codex` → `.gemini`), so a wiki configured under pi
|
|
23
|
+
* would silently lose its settings after switching hosts. This module keeps
|
|
24
|
+
* both layouts readable and picks a sensible file to write to.
|
|
25
|
+
*
|
|
26
|
+
* Everything here is additive: on pi with only a `.pi/` directory the effective
|
|
27
|
+
* behaviour is identical to the pre-compat code path, which keeps upstream
|
|
28
|
+
* merges clean.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type HostKind = "pi" | "omp";
|
|
32
|
+
|
|
33
|
+
/** Project config directory name per host. */
|
|
34
|
+
const CONFIG_DIR: Record<HostKind, string> = { pi: ".pi", omp: ".omp" };
|
|
35
|
+
|
|
36
|
+
/** Settings file names inside a config directory, lowest → highest precedence. */
|
|
37
|
+
const SETTINGS_FILES = ["settings.json", "config.yml", "config.yaml"] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect which agent is hosting this extension.
|
|
41
|
+
*
|
|
42
|
+
* Ordered by reliability:
|
|
43
|
+
* 1. `LLM_WIKI_HOST` — explicit escape hatch (tests, exotic embeddings).
|
|
44
|
+
* 2. The agent directory path: pi resolves `~/.pi/agent`, omp `~/.omp/agent`.
|
|
45
|
+
* A `PI_CODING_AGENT_DIR` override that keeps the marker segment still
|
|
46
|
+
* classifies correctly; anything else falls through.
|
|
47
|
+
* 3. `OMP_PROFILE`, which omp sets on itself whenever a profile is active.
|
|
48
|
+
* 4. Default `pi` — the historical behaviour.
|
|
49
|
+
*/
|
|
50
|
+
export function detectHost(): HostKind {
|
|
51
|
+
const forced = process.env.LLM_WIKI_HOST?.trim().toLowerCase();
|
|
52
|
+
if (forced === "omp" || forced === "pi") return forced;
|
|
53
|
+
|
|
54
|
+
let agentDir = "";
|
|
55
|
+
try {
|
|
56
|
+
agentDir = getAgentDir();
|
|
57
|
+
} catch {
|
|
58
|
+
agentDir = "";
|
|
59
|
+
}
|
|
60
|
+
if (agentDir) {
|
|
61
|
+
const segments = agentDir.split(/[\\/]/);
|
|
62
|
+
if (segments.includes(".omp")) return "omp";
|
|
63
|
+
if (segments.includes(".pi")) return "pi";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (process.env.OMP_PROFILE !== undefined) return "omp";
|
|
67
|
+
return "pi";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every project settings file that may hold `llm-wiki` configuration, ordered
|
|
72
|
+
* from lowest to highest precedence so callers can merge left-to-right.
|
|
73
|
+
*
|
|
74
|
+
* The host's *native* directory is last (wins). The foreign directory is still
|
|
75
|
+
* read so a vault configured under pi keeps working after omp takes over the
|
|
76
|
+
* repository, and vice versa. Within a directory `config.yml` follows
|
|
77
|
+
* `settings.json`, matching omp's own project-settings precedence.
|
|
78
|
+
*/
|
|
79
|
+
export function listProjectSettingsFiles(cwd: string, host: HostKind = detectHost()): string[] {
|
|
80
|
+
const foreign: HostKind = host === "omp" ? "pi" : "omp";
|
|
81
|
+
const files: string[] = [];
|
|
82
|
+
for (const kind of [foreign, host]) {
|
|
83
|
+
const dir = join(cwd, CONFIG_DIR[kind]);
|
|
84
|
+
for (const name of SETTINGS_FILES) files.push(join(dir, name));
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* User-level settings files, lowest → highest precedence.
|
|
91
|
+
*
|
|
92
|
+
* `getAgentDir()` already resolves per host (`~/.pi/agent` vs `~/.omp/agent`),
|
|
93
|
+
* so only the file names differ: omp migrates `settings.json` into `config.yml`
|
|
94
|
+
* on first start, and a migrated install has *only* the YAML file.
|
|
95
|
+
*/
|
|
96
|
+
export function listGlobalSettingsFiles(): string[] {
|
|
97
|
+
let agentDir = "";
|
|
98
|
+
try {
|
|
99
|
+
agentDir = getAgentDir();
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
if (!agentDir) return [];
|
|
104
|
+
return SETTINGS_FILES.map((name) => join(agentDir, name));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The project settings file this extension writes to.
|
|
109
|
+
*
|
|
110
|
+
* Always JSON (`settings.json`) — both hosts read it, and rewriting a user's
|
|
111
|
+
* hand-authored `config.yml` would destroy comments and formatting.
|
|
112
|
+
*
|
|
113
|
+
* Directory choice: an already-existing project config directory wins (so a
|
|
114
|
+
* repo that only has `.pi/` keeps a single settings file), otherwise the
|
|
115
|
+
* detected host's native directory is created.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveProjectSettingsPath(cwd: string, host: HostKind = detectHost()): string {
|
|
118
|
+
const native = join(cwd, CONFIG_DIR[host]);
|
|
119
|
+
if (existsSync(native)) return join(native, "settings.json");
|
|
120
|
+
|
|
121
|
+
const foreign = join(cwd, CONFIG_DIR[host === "omp" ? "pi" : "omp"]);
|
|
122
|
+
if (existsSync(foreign)) return join(foreign, "settings.json");
|
|
123
|
+
|
|
124
|
+
return join(native, "settings.json");
|
|
125
|
+
}
|
|
@@ -322,11 +322,20 @@ export function buildReminderText(): string {
|
|
|
322
322
|
* `options.display` (issue #77) controls whether the reminder is shown to the
|
|
323
323
|
* user (`true`, the default) or injected silently into model context only
|
|
324
324
|
* (`false`). Pass a resolver so the live `notices` config is read at send time.
|
|
325
|
+
*
|
|
326
|
+
* `options.enabled` gates the reminder entirely — note that `display: false`
|
|
327
|
+
* still injects it into model context, so it is NOT a way to switch the
|
|
328
|
+
* reminder off. Callers pass a resolver that answers "does a wiki apply to the
|
|
329
|
+
* current working directory", evaluated per turn because the session can move.
|
|
325
330
|
*/
|
|
326
331
|
export function registerObservationReminder(
|
|
327
332
|
pi: ExtensionAPI,
|
|
328
333
|
reminderState: ReminderState,
|
|
329
|
-
options?: {
|
|
334
|
+
options?: {
|
|
335
|
+
turnsBetweenReminders?: number;
|
|
336
|
+
display?: boolean | (() => boolean);
|
|
337
|
+
enabled?: () => boolean;
|
|
338
|
+
},
|
|
330
339
|
): void {
|
|
331
340
|
const REMINDER_INTERVAL = options?.turnsBetweenReminders ?? 5;
|
|
332
341
|
const resolveDisplay = (): boolean => {
|
|
@@ -355,6 +364,10 @@ export function registerObservationReminder(
|
|
|
355
364
|
// errors cause multiple retries, each firing agent_end).
|
|
356
365
|
if ("willRetry" in event && (event as { willRetry?: boolean }).willRetry) return;
|
|
357
366
|
|
|
367
|
+
// No wiki applies here: never nag, and never accumulate a pending reminder
|
|
368
|
+
// that would fire the moment the session moves into a wiki-bearing project.
|
|
369
|
+
if (options?.enabled && !options.enabled()) return;
|
|
370
|
+
|
|
358
371
|
turnsSinceLastReminder++;
|
|
359
372
|
if (turnsSinceLastReminder < REMINDER_INTERVAL) return;
|
|
360
373
|
if (reminderState.observeDoneThisSession) return;
|