pi-lean-dimension 0.3.3 → 0.4.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 +17 -24
- package/node_modules/pi-lean-portal/README.md +20 -45
- package/node_modules/pi-lean-portal/browser-toggle.ts +10 -101
- package/node_modules/pi-lean-portal/package.json +2 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +2 -5
- package/node_modules/pi-lean-search/index.ts +1 -11
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-tool-masking/README.md +69 -10
- package/node_modules/pi-tool-masking/index.ts +730 -39
- package/node_modules/pi-tool-masking/package.json +1 -2
- package/package.json +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-migration.test.ts +0 -145
package/README.md
CHANGED
|
@@ -21,34 +21,27 @@ npx playwright install chromium firefox
|
|
|
21
21
|
That's it — the tools are registered and start enabled by default. Control
|
|
22
22
|
them with `/web on|off|learn` (`on` = browser tools, `learn` = browser tools
|
|
23
23
|
plus guide-saving via `web-learn`, `off` = everything off). The state persists
|
|
24
|
-
per session. To set a different default for **new** sessions, add
|
|
25
|
-
Pi settings (`~/.pi/agent/settings.json` or
|
|
24
|
+
per session. To set a different default for **new** sessions, add a
|
|
25
|
+
`toolsetDefaults` block to your Pi settings (`~/.pi/agent/settings.json` or
|
|
26
|
+
`.pi/settings.json`):
|
|
26
27
|
|
|
27
28
|
```json
|
|
28
|
-
{
|
|
29
|
+
{
|
|
30
|
+
"toolsetDefaults": {
|
|
31
|
+
"toolset-state:pi-lean-dimension.web": { "enabled": false },
|
|
32
|
+
"toolset-state:pi-lean-dimension.web-learn": { "enabled": false },
|
|
33
|
+
"toolset-state:pi-lean-dimension.search": { "enabled": false }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
```
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
>
|
|
39
|
-
> ```json
|
|
40
|
-
> {
|
|
41
|
-
> "browserToggle": { "defaultEnabled": false },
|
|
42
|
-
> "toolsetDefaults": {
|
|
43
|
-
> "toolset-state:pi-lean-dimension.web": { "enabled": false },
|
|
44
|
-
> "toolset-state:pi-lean-dimension.web-learn": { "enabled": false },
|
|
45
|
-
> "toolset-state:pi-lean-dimension.search": { "enabled": false }
|
|
46
|
-
> }
|
|
47
|
-
> }
|
|
48
|
-
> ```
|
|
49
|
-
>
|
|
50
|
-
> (omit a `toolsetDefaults` key to use the toolset's packaged default; the
|
|
51
|
-
> `search` key only applies when `pi-lean-search` is installed.)
|
|
38
|
+
`toolsetDefaults` is read by the [`pi-tool-masking`](https://github.com/coreyryanhanson/pi-tool-masking/) library on restore, before
|
|
39
|
+
the toolset's packaged default. Omit a key to use the packaged default; the
|
|
40
|
+
`search` key only applies when `pi-lean-search` is installed.
|
|
41
|
+
|
|
42
|
+
> The legacy `browserToggle.defaultEnabled` key is **removed in 0.4.0** and
|
|
43
|
+
> no longer read. If you pinned it, move the value into the matching
|
|
44
|
+
> `toolsetDefaults` entry above.
|
|
52
45
|
|
|
53
46
|
---
|
|
54
47
|
|
|
@@ -115,17 +115,10 @@ toggles guide-saving mode, and manages browser profiles.
|
|
|
115
115
|
### Persistence
|
|
116
116
|
|
|
117
117
|
The toggle state is stored in the conversation's branch history. A fresh
|
|
118
|
-
conversation starts with the default from `settings.json`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
> replaced by the `pi-tool-masking` library's `toolsetDefaults` block in an
|
|
123
|
-
> upcoming release — **that block is not read yet**, so keep the legacy key
|
|
124
|
-
> until the replacement ships. See
|
|
125
|
-
> [Configuration (`settings.json`)](#configuration-settingsjson) for the
|
|
126
|
-
> future shape and how to pre-add it harmlessly. A migration warning fires on
|
|
127
|
-
> session start once the legacy key is present and the web default hasn't yet
|
|
128
|
-
> been mirrored into `toolsetDefaults`.
|
|
118
|
+
conversation starts with the default from `settings.json` — the
|
|
119
|
+
`pi-tool-masking` library's `toolsetDefaults` block (see
|
|
120
|
+
[Configuration (`settings.json`)](#configuration-settingsjson)), falling back
|
|
121
|
+
to the toolset's packaged default (`true` for `web`).
|
|
129
122
|
|
|
130
123
|
---
|
|
131
124
|
|
|
@@ -615,48 +608,30 @@ a `profile` parameter:
|
|
|
615
608
|
}
|
|
616
609
|
```
|
|
617
610
|
|
|
618
|
-
### `
|
|
611
|
+
### `toolsetDefaults` *(settings-based toolset defaults)*
|
|
619
612
|
|
|
620
|
-
Whether browser tools are enabled on fresh conversations.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
**that block is not read yet**, so keep this key until the replacement ships.
|
|
613
|
+
Whether browser tools are enabled on fresh conversations. Read by the
|
|
614
|
+
`pi-tool-masking` library at restore time, between the chat-branch tier and
|
|
615
|
+
the toolset's packaged default:
|
|
624
616
|
|
|
625
617
|
```jsonc
|
|
626
618
|
{
|
|
627
|
-
"
|
|
628
|
-
"
|
|
619
|
+
"toolsetDefaults": {
|
|
620
|
+
"toolset-state:pi-lean-dimension.web": { "enabled": true },
|
|
621
|
+
"toolset-state:pi-lean-dimension.web-learn": { "enabled": false },
|
|
622
|
+
"toolset-state:pi-lean-dimension.search": { "enabled": true }
|
|
629
623
|
}
|
|
630
624
|
}
|
|
631
625
|
```
|
|
632
626
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
>
|
|
639
|
-
>
|
|
640
|
-
>
|
|
641
|
-
> {
|
|
642
|
-
> "browserToggle": { "defaultEnabled": true },
|
|
643
|
-
> "toolsetDefaults": {
|
|
644
|
-
> "toolset-state:pi-lean-dimension.web": { "enabled": true },
|
|
645
|
-
> "toolset-state:pi-lean-dimension.web-learn": { "enabled": false },
|
|
646
|
-
> "toolset-state:pi-lean-dimension.search": { "enabled": true }
|
|
647
|
-
> }
|
|
648
|
-
> }
|
|
649
|
-
> ```
|
|
650
|
-
>
|
|
651
|
-
> - Keys are the toolsets' `persistKey` values (`toolset-state:<id>`).
|
|
652
|
-
> - Omit a `toolsetDefaults` key to use the toolset's packaged default (`web`
|
|
653
|
-
> and `search` default `true`; `web-learn` defaults `false`).
|
|
654
|
-
> - The `search` key only applies when `pi-lean-search` is installed.
|
|
655
|
-
>
|
|
656
|
-
> Once the offload ships and the legacy read is removed, delete the
|
|
657
|
-
> `browserToggle` block. Until then, a migration warning fires on session
|
|
658
|
-
> start when the legacy key is present and the web default hasn't yet been
|
|
659
|
-
> mirrored into `toolsetDefaults`; mirroring it suppresses the warning.
|
|
627
|
+
- Keys are the toolsets' `persistKey` values (`toolset-state:<id>`).
|
|
628
|
+
- Omit a `toolsetDefaults` key to use the toolset's packaged default (`web`
|
|
629
|
+
and `search` default `true`; `web-learn` defaults `false`).
|
|
630
|
+
- The `search` key only applies when `pi-lean-search` is installed.
|
|
631
|
+
|
|
632
|
+
> The legacy `browserToggle.defaultEnabled` key is **removed in 0.4.0** — it
|
|
633
|
+
> is no longer read. If you pinned it, move the value into the matching
|
|
634
|
+
> `toolsetDefaults` entry above.
|
|
660
635
|
|
|
661
636
|
### `browser.maxStorageStateSize`
|
|
662
637
|
|
|
@@ -9,24 +9,12 @@ import {
|
|
|
9
9
|
getDefaultResolutionMode,
|
|
10
10
|
} from "pi-tool-masking";
|
|
11
11
|
import type { ToolsetSpec } from "pi-tool-masking";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// ships in pi-tool-masking 1.2.0), so the string cast is load-bearing: an
|
|
17
|
-
// allowlist-capable consumer sharing the `globalThis` module state writes
|
|
18
|
-
// `"allowlist"` into it, and this consumer reads that value back at runtime
|
|
19
|
-
// even though its own bundled type doesn't name the mode. On published
|
|
20
|
-
// versions no caller ever writes `"allowlist"`, so this is a no-op for
|
|
21
|
-
// ordinary users — it only activates when an allowlist-capable
|
|
22
|
-
// pi-tool-masking consumer is in play.
|
|
23
|
-
//
|
|
24
|
-
// Cleanup at the ^1.2.0 bump: once `DefaultResolutionMode` names
|
|
25
|
-
// `"allowlist"`, drop the `as string` cast here and in pi-lean-search's
|
|
26
|
-
// co-activation mirror — the type system can then check the comparison
|
|
27
|
-
// directly and the cast becomes a suppressor of a check it should perform.
|
|
12
|
+
|
|
13
|
+
// Focus-mode guard: refuse actuating subcommands while the library holds the
|
|
14
|
+
// line — inclusion mode or allowlist focus (an upstream pi-tool-masking
|
|
15
|
+
// consumer).
|
|
28
16
|
function isFocusHolding(): boolean {
|
|
29
|
-
const mode = getDefaultResolutionMode()
|
|
17
|
+
const mode = getDefaultResolutionMode();
|
|
30
18
|
return mode === "inclusion" || mode === "allowlist";
|
|
31
19
|
}
|
|
32
20
|
|
|
@@ -154,86 +142,13 @@ function renderBrowserGlyph(
|
|
|
154
142
|
}
|
|
155
143
|
}
|
|
156
144
|
|
|
157
|
-
// ---- Legacy settings migration warning ---------------------------
|
|
158
|
-
//
|
|
159
|
-
// The portal currently seeds the web toolset's fresh-session default from
|
|
160
|
-
// `browserToggle.defaultEnabled` in settings.json. An upcoming pi-tool-masking
|
|
161
|
-
// release takes over settings-based toolset defaults and reads a new
|
|
162
|
-
// `toolsetDefaults` block instead. Warn users who pinned the legacy key so
|
|
163
|
-
// they can migrate before the offload lands and the legacy read is removed.
|
|
164
|
-
//
|
|
165
|
-
// ponytail: warn-only — we still honor the legacy key for backward compat
|
|
166
|
-
// until pi-tool-masking owns the settings tier; then delete this helper and
|
|
167
|
-
// the `browserToggle` read below. Ceiling: a silent default shift for users
|
|
168
|
-
// who ignore the warning; upgrade path is the pi-tool-masking bump.
|
|
169
|
-
const TOOLSET_DEFAULTS_MIGRATION_MSG =
|
|
170
|
-
"⚠️ pi-lean-portal: settings-based toolset defaults are moving to the " +
|
|
171
|
-
"pi-tool-masking library. The `browserToggle.defaultEnabled` key in your " +
|
|
172
|
-
"settings.json will stop being read in an upcoming release. It still works " +
|
|
173
|
-
"today — KEEP it until the replacement ships, and add the new " +
|
|
174
|
-
"`toolsetDefaults` block alongside it so your config is ready (the new " +
|
|
175
|
-
"block is not read yet, but is harmless now and will take effect " +
|
|
176
|
-
"automatically once the offload lands):\n\n" +
|
|
177
|
-
"{\n" +
|
|
178
|
-
' "browserToggle": { "defaultEnabled": true },\n' +
|
|
179
|
-
' "toolsetDefaults": {\n' +
|
|
180
|
-
' "toolset-state:pi-lean-dimension.web": { "enabled": true },\n' +
|
|
181
|
-
' "toolset-state:pi-lean-dimension.web-learn": { "enabled": true },\n' +
|
|
182
|
-
' "toolset-state:pi-lean-dimension.search": { "enabled": true }\n' +
|
|
183
|
-
" }\n" +
|
|
184
|
-
"}\n" +
|
|
185
|
-
"(omit a `toolsetDefaults` key to use the toolset's packaged default; the " +
|
|
186
|
-
"`search` key only applies when pi-lean-search is installed). Mirror your " +
|
|
187
|
-
"web default into `toolset-state:pi-lean-dimension.web` to silence this " +
|
|
188
|
-
"warning.";
|
|
189
|
-
|
|
190
|
-
function hasLegacyToolsetDefault(merged: Record<string, unknown>): boolean {
|
|
191
|
-
const seg = merged["browserToggle"];
|
|
192
|
-
if (!seg || typeof seg !== "object" || Array.isArray(seg)) return false;
|
|
193
|
-
return (
|
|
194
|
-
typeof (seg as Record<string, unknown>)["defaultEnabled"] === "boolean"
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// The new `toolsetDefaults` block the pi-tool-masking offload will read.
|
|
199
|
-
// When the user has already migrated the web toolset's default into it,
|
|
200
|
-
// the warning is redundant even if the legacy `browserToggle.defaultEnabled`
|
|
201
|
-
// key is still sitting on disk — suppress so a migrated settings file stays
|
|
202
|
-
// quiet. Only the web persist key gates this: it's the one the legacy key
|
|
203
|
-
// controlled, so its presence means the user acted on the migration.
|
|
204
|
-
const WEB_TOOLSET_PERSIST_KEY = "toolset-state:pi-lean-dimension.web";
|
|
205
|
-
function hasMigratedToolsetDefault(merged: Record<string, unknown>): boolean {
|
|
206
|
-
const td = merged["toolsetDefaults"];
|
|
207
|
-
if (!td || typeof td !== "object" || Array.isArray(td)) return false;
|
|
208
|
-
const entry = (td as Record<string, unknown>)[WEB_TOOLSET_PERSIST_KEY];
|
|
209
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
210
|
-
return typeof (entry as Record<string, unknown>)["enabled"] === "boolean";
|
|
211
|
-
}
|
|
212
|
-
|
|
213
145
|
// ---- Toggle initializer ------------------------------------------
|
|
214
146
|
|
|
215
147
|
export default function initBrowserToggle(pi: ExtensionAPI) {
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
// key is dead weight — no need to nag.
|
|
221
|
-
const legacyDefaultPinned =
|
|
222
|
-
hasLegacyToolsetDefault(merged) && !hasMigratedToolsetDefault(merged);
|
|
223
|
-
const browserToggleSegment = (merged as Record<string, unknown>)[
|
|
224
|
-
"browserToggle"
|
|
225
|
-
] as Record<string, unknown> | undefined;
|
|
226
|
-
const webDefault =
|
|
227
|
-
browserToggleSegment &&
|
|
228
|
-
typeof browserToggleSegment["defaultEnabled"] === "boolean"
|
|
229
|
-
? (browserToggleSegment["defaultEnabled"] as boolean)
|
|
230
|
-
: true;
|
|
231
|
-
|
|
232
|
-
const webSpec: ToolsetSpec = {
|
|
233
|
-
...PORTAL_WEB_SPEC,
|
|
234
|
-
defaultEnabled: webDefault,
|
|
235
|
-
};
|
|
236
|
-
const webToolset = defineToolset(pi, webSpec);
|
|
148
|
+
// Settings-based toolset defaults (`toolsetDefaults` tier) are read by
|
|
149
|
+
// pi-tool-masking itself inside defineToolset/restore — pass the packaged
|
|
150
|
+
// spec straight through.
|
|
151
|
+
const webToolset = defineToolset(pi, PORTAL_WEB_SPEC);
|
|
237
152
|
const learnToolset = defineToolset(pi, PORTAL_LEARN_SPEC);
|
|
238
153
|
|
|
239
154
|
// ── Keep cached state in sync with library events ─────────
|
|
@@ -267,8 +182,7 @@ export default function initBrowserToggle(pi: ExtensionAPI) {
|
|
|
267
182
|
// unguarded, matching the focus controller's treatment of its own
|
|
268
183
|
// read-only commands.
|
|
269
184
|
if (["on", "off", "learn"].includes(cmd) && isFocusHolding()) {
|
|
270
|
-
const inInclusion =
|
|
271
|
-
(getDefaultResolutionMode() as string) === "inclusion";
|
|
185
|
+
const inInclusion = getDefaultResolutionMode() === "inclusion";
|
|
272
186
|
ctx.ui.notify(
|
|
273
187
|
inInclusion
|
|
274
188
|
? "Another plugin has active inclusion mode — this toolset can't be toggled while inclusion is holding the line. Deactivate it there first."
|
|
@@ -348,11 +262,6 @@ export default function initBrowserToggle(pi: ExtensionAPI) {
|
|
|
348
262
|
restoreProfile(pi, ctx);
|
|
349
263
|
_lastCtx = ctx;
|
|
350
264
|
syncCachedState();
|
|
351
|
-
// One-time-per-session migration warning when the legacy
|
|
352
|
-
// `browserToggle.defaultEnabled` pin is still on disk.
|
|
353
|
-
if (legacyDefaultPinned) {
|
|
354
|
-
ctx.ui.notify(TOOLSET_DEFAULTS_MIGRATION_MSG, "warning");
|
|
355
|
-
}
|
|
356
265
|
});
|
|
357
266
|
|
|
358
267
|
pi.on("session_tree", async (_event, ctx) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-portal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension. Interactive web browsing for Pi — a /web toggle removes the tools from context when switched off, Playwright Chromium/Firefox deliver accessibility-tree snapshots, persistent profiles, cookies, and domain-matched guides; custom/stealth backends (Camoufox) plug in when a site blocks the shipped browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"node-html-parser": "^6.1.0",
|
|
59
|
-
"pi-tool-masking": "^1.
|
|
59
|
+
"pi-tool-masking": "^1.2.0",
|
|
60
60
|
"playwright": "^1.60.0",
|
|
61
61
|
"turndown": "^7.2.0"
|
|
62
62
|
},
|
|
@@ -80,9 +80,7 @@ describe("readSearxngUrl", () => {
|
|
|
80
80
|
vi.mocked(existsSync).mockImplementation(
|
|
81
81
|
(path) => typeof path === "string" && path.includes(".pi"),
|
|
82
82
|
);
|
|
83
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
84
|
-
JSON.stringify({ theme: "dark", browserToggle: {} }),
|
|
85
|
-
);
|
|
83
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ theme: "dark" }));
|
|
86
84
|
expect(readSearxngUrl()).toBeUndefined();
|
|
87
85
|
});
|
|
88
86
|
|
|
@@ -482,8 +480,7 @@ describe("pi-lean-dimension.web co-activation mirror", () => {
|
|
|
482
480
|
// Allowlist focus (an upstream pi-tool-masking consumer) holds the line —
|
|
483
481
|
// the mirror must not co-activate, so a stale library `doRestore` emitting
|
|
484
482
|
// a web `changed` during resume can't disable search or write a {enabled}
|
|
485
|
-
// entry. Set the shared module state directly
|
|
486
|
-
// "allowlist").
|
|
483
|
+
// entry. Set the shared module state directly.
|
|
487
484
|
it("skips co-activation while allowlist focus is active", async () => {
|
|
488
485
|
const { pi, events } = mockSearchPi(["web-search"]);
|
|
489
486
|
searchExtension(pi);
|
|
@@ -208,20 +208,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
208
|
// co-activation. The focus set is authoritative, so a web `changed` event
|
|
209
209
|
// — including one a stale library `doRestore` emits during resume — must
|
|
210
210
|
// not disable search or write a focus-indistinguishable {enabled} entry.
|
|
211
|
-
// The published `DefaultResolutionMode` type doesn't name `"allowlist"`
|
|
212
|
-
// (it ships in pi-tool-masking 1.2.0), so the string cast is load-bearing:
|
|
213
|
-
// an allowlist-capable consumer sharing the `globalThis` module state
|
|
214
|
-
// writes `"allowlist"` into it and we read it back here. No-op for
|
|
215
|
-
// ordinary users on published versions, where nothing ever writes
|
|
216
|
-
// `"allowlist"`.
|
|
217
|
-
//
|
|
218
|
-
// Cleanup at the ^1.2.0 bump: drop the `as string` cast once
|
|
219
|
-
// `DefaultResolutionMode` names `"allowlist"` — same as the matching cast
|
|
220
|
-
// in pi-lean-portal's browser-toggle focus guard.
|
|
221
211
|
pi.events.on(TOOLSET_EVENTS.changed, (data: unknown) => {
|
|
222
212
|
const event = data as ToolsetChangedEvent;
|
|
223
213
|
if (event.id === "pi-lean-dimension.web") {
|
|
224
|
-
if (
|
|
214
|
+
if (getDefaultResolutionMode() === "allowlist") return;
|
|
225
215
|
if (event.enabled) {
|
|
226
216
|
searchToolset.enable(pi);
|
|
227
217
|
} else {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension. SearXNG search tool for Pi — pairs with pi-lean-portal's /web toggle; search-only installs are valid, or add it to a portal install for the full web-tools suite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test": "vitest run"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"pi-tool-masking": "^1.
|
|
41
|
+
"pi-tool-masking": "^1.2.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
|
@@ -16,7 +16,7 @@ Without `pi-tool-masking`, every pi extension that toggles tools reimplements th
|
|
|
16
16
|
4. On `session_start` / `session_tree`, walk the branch for persisted entries and re-apply state.
|
|
17
17
|
5. Emit events so side-effect owners (status bars, pickers) can re-render.
|
|
18
18
|
|
|
19
|
-
`pi-tool-masking` does all of that in one call: `defineToolset(pi, spec)`. It also adds dependency cascading (enabling a toolset auto-enables its dependencies) and reverse cascading (disabling a toolset auto-disables dependents).
|
|
19
|
+
`pi-tool-masking` does all of that in one call: `defineToolset(pi, spec)`. It also adds dependency cascading (enabling a toolset auto-enables its dependencies) and reverse cascading (disabling a toolset auto-disables dependents), plus a settings tier for per-scope defaults and allowlist mode for reliable focus across reloads.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -89,19 +89,24 @@ Register a toolset and receive a `Toolset` handle (`enable`, `disable`, `isEnabl
|
|
|
89
89
|
|
|
90
90
|
**Idempotent re-registration:** calling `defineToolset` with the same `spec.id` and an unchanged spec returns the existing toolset. This is safe across `/reload`.
|
|
91
91
|
|
|
92
|
-
### `setDefaultResolutionMode(pi, mode)`
|
|
92
|
+
### `setDefaultResolutionMode(pi, mode, allowlist?)`
|
|
93
93
|
|
|
94
|
-
Switch how toolsets with no persisted state resolve on restore.
|
|
94
|
+
Switch how toolsets with no persisted state resolve on restore. Three modes:
|
|
95
95
|
|
|
96
96
|
| Mode | Behavior on restore (no persisted entry) |
|
|
97
97
|
|---|---|
|
|
98
98
|
| `"exclusion"` (default) | Toolsets default **on** if `defaultEnabled` is true, **off** otherwise |
|
|
99
|
-
| `"inclusion"` | All unknown toolsets default **off** —
|
|
99
|
+
| `"inclusion"` (@deprecated since 1.2.0) | All unknown toolsets default **off** — a weaker, unbounded floor. Use `"allowlist"` instead for focus-style "only these tools" suppression |
|
|
100
|
+
| `"allowlist"` | Only the listed toolset ids are **on**, everything else **off** — a finite, branch-persisted set whose complement is computed at restore, resilient to toolsets installed later. Pass the array as the third argument: `setDefaultResolutionMode(pi, "allowlist", ["my-plugin.web"])` |
|
|
100
101
|
|
|
101
102
|
### `getDefaultResolutionMode()`
|
|
102
103
|
|
|
103
104
|
Read the current default resolution mode. Returns `"exclusion"` until a session restore loads the persisted mode.
|
|
104
105
|
|
|
106
|
+
### `getActiveAllowlist()`
|
|
107
|
+
|
|
108
|
+
Read the live allowlist array from module state. Returns the `string[]` when the active mode is `"allowlist"`, otherwise `undefined`. Parameterless (like `getDefaultResolutionMode`) — the downstream call site receives `ExtensionAPI`, which doesn't expose `sessionManager`, so the branch can't be read there. The branch remains the source of truth; module state is the live mirror, snapped from the last mode branch entry by `doRestore` (and by `setDefaultResolutionMode` when it appends the entry). A consumer consults this to keep toolsets registered *after* focus was entered off.
|
|
109
|
+
|
|
105
110
|
### `getRegisteredToolsets()`
|
|
106
111
|
|
|
107
112
|
Return a read-only snapshot of every registered toolset (`{ spec, toolset }`). No `pi` argument needed — pure registry read.
|
|
@@ -121,10 +126,59 @@ Return a read-only snapshot of every registered toolset (`{ spec, toolset }`). N
|
|
|
121
126
|
| `Toolset` | Handle returned by `defineToolset` |
|
|
122
127
|
| `ToolsetChangedEvent` | Shape of events emitted by `TOOLSET_EVENTS` |
|
|
123
128
|
| `RegistryEntry` | `{ spec: ToolsetSpec; toolset: Toolset }` — a single registered toolset |
|
|
124
|
-
| `DefaultResolutionMode` | `"exclusion" \| "inclusion"` |
|
|
129
|
+
| `DefaultResolutionMode` | `"exclusion" \| "inclusion" \| "allowlist"` |
|
|
130
|
+
| `MalformedSettingsError` | Thrown by `writeToolsetDefaults` / `clearToolsetDefaults` when settings.json is corrupt or non-object (never silently overwritten). Catch with `instanceof`. |
|
|
125
131
|
|
|
126
132
|
---
|
|
127
133
|
|
|
134
|
+
## Toolset defaults (settings tier)
|
|
135
|
+
|
|
136
|
+
A toolset's fresh-session default is no longer locked to its packaged `spec.defaultEnabled`. Users can pin `{ enabled: boolean }` under a reserved `toolsetDefaults` key in pi-core settings, keyed by the toolset's full `persistKey` — without toggling (which writes a session-scoped chat-branch entry). The library reads both files itself inside `doRestore`, fresh on each `/reload`, so downstream consumers no longer need to reinvent a settings reader to inject values into `spec.defaultEnabled` before `defineToolset`.
|
|
137
|
+
|
|
138
|
+
Restore resolves each toolset's default in this order (first hit wins):
|
|
139
|
+
|
|
140
|
+
1. **Chat-branch entry** — the last `appendEntry(persistKey, …)` on this branch. A `null` tombstone (see [`clearToolsetEntry`](#tombstone-helpers)) falls through to tier 2.
|
|
141
|
+
2. **Settings pin** — `toolsetDefaults[persistKey].enabled`, merged global → project (project wins per entry). Mode-agnostic.
|
|
142
|
+
3. **Packaged default** — `spec.defaultEnabled ?? true`, filtered by resolution mode for unpinned toolsets only.
|
|
143
|
+
|
|
144
|
+
Settings pins are honored in all three modes, mirroring how chat-branch entries are honored — only unpinned toolsets consult mode for the floor.
|
|
145
|
+
|
|
146
|
+
### `readMergedToolsetDefaults()`
|
|
147
|
+
|
|
148
|
+
Read and merge `toolsetDefaults` from global (`~/.pi/agent/settings.json`, or `$PI_CODING_AGENT_DIR/settings.json`) and project (`<cwd>/.pi/settings.json`) settings. Project overrides global per entry. Missing/unreadable/malformed files contribute `{}`. Never throws. Returns `Record<persistKey, { enabled: boolean }>`. Read once per loop and pass the snapshot to `getEffectiveDefault` to avoid re-reading disk per toolset.
|
|
149
|
+
|
|
150
|
+
### `readToolsetDefaults(scope)`
|
|
151
|
+
|
|
152
|
+
Read one scope's raw `toolsetDefaults` block (no merge). `scope` is `"global"` or `"project"`. Same never-throw policy. Use for `defaults show`-style commands that need per-scope attribution.
|
|
153
|
+
|
|
154
|
+
### `writeToolsetDefaults(entries, scope)`
|
|
155
|
+
|
|
156
|
+
Merge a batch of `{ [persistKey]: { enabled } }` entries into one scope's `toolsetDefaults`, preserving every other top-level key and every existing entry not in `entries`. A write where every entry already matches its on-disk value is a no-op (no reformat, no mtime bump). Returns the settings file path. **Throws `MalformedSettingsError`** if the file exists but parses to a non-object or is unparsable — a corrupt file is never silently overwritten.
|
|
157
|
+
|
|
158
|
+
### `clearToolsetDefaults(scope)`
|
|
159
|
+
|
|
160
|
+
Remove the `toolsetDefaults` wrapper key entirely from one scope, preserving every other top-level key. Returns the path removed from, or `null` if the key was already absent (or the file missing). No per-entry clear by design — write an `entries` map without the unwanted keys via `writeToolsetDefaults`. Same `MalformedSettingsError` guard.
|
|
161
|
+
|
|
162
|
+
### `getEffectiveDefault(spec, snapshot?)`
|
|
163
|
+
|
|
164
|
+
Resolve a toolset's effective fresh-session default: settings tier (2) then packaged `spec.defaultEnabled ?? true` (3). **Ignores resolution mode** — callers needing mode-aware behavior must consult `getDefaultResolutionMode()` themselves. Pass an explicit `snapshot` (from `readMergedToolsetDefaults()`) when looping over multiple toolsets; omit it for a one-off (it performs its own read).
|
|
165
|
+
|
|
166
|
+
## Tombstone helpers
|
|
167
|
+
|
|
168
|
+
Within pi-core's append-only `SessionManager`, a toolset's chat-branch entry can't be deleted — but a `null` tombstone appended after the last entry makes `doRestore` fall through to the settings tier so settings re-assert. Tombstones are dedup'd (no-op when the last entry is already cleared) and never written for never-toggled toolsets. A later manual toggle appends after the tombstone and supersedes it.
|
|
169
|
+
|
|
170
|
+
### `clearToolsetEntry(pi, persistKey, branch)`
|
|
171
|
+
|
|
172
|
+
Append a `null` tombstone for one toolset's branch entry (dedup'd). `branch` is the caller's `ctx.sessionManager.getBranch()` snapshot — `ExtensionAPI` exposes `appendEntry` but not `sessionManager`, so the dedup read comes from the caller.
|
|
173
|
+
|
|
174
|
+
### `clearAllToolsetEntries(pi, branch)`
|
|
175
|
+
|
|
176
|
+
Tombstone every registered toolset's branch entry (dedup'd per toolset). Covers exactly the toolsets in the global registry.
|
|
177
|
+
|
|
178
|
+
### `applyToolsetEnabled(pi, spec, enabled)`
|
|
179
|
+
|
|
180
|
+
Apply a toolset's enabled state via `setActiveTools` and emit `TOOLSET_EVENTS.changed` **without** writing a branch entry — the live-apply half of a settings restore (pull a toolset to its settings/packaged default without persisting a chat-branch pin).
|
|
181
|
+
|
|
128
182
|
## ToolsetSpec fields
|
|
129
183
|
|
|
130
184
|
```ts
|
|
@@ -206,15 +260,18 @@ pi.events.on(TOOLSET_EVENTS.restored, (event) => {
|
|
|
206
260
|
});
|
|
207
261
|
```
|
|
208
262
|
|
|
209
|
-
### Focus mode (
|
|
263
|
+
### Focus mode (allowlist resolution)
|
|
210
264
|
|
|
211
265
|
```ts
|
|
212
266
|
import { setDefaultResolutionMode, getRegisteredToolsets } from "pi-tool-masking";
|
|
213
267
|
|
|
214
|
-
// Enter focus:
|
|
215
|
-
|
|
268
|
+
// Enter focus: allowlist mode keeps only the listed toolsets on — restore
|
|
269
|
+
// applies it on the next /reload, and the loop below applies it live.
|
|
270
|
+
// "inclusion" (deprecated) cannot guarantee this: a toolset installed after
|
|
271
|
+
// focus leaks on, because the set of "on" toolsets was never recorded.
|
|
272
|
+
setDefaultResolutionMode(pi, "allowlist", ["my-plugin.web"]);
|
|
216
273
|
|
|
217
|
-
//
|
|
274
|
+
// Apply live: enable only the allowlisted toolsets
|
|
218
275
|
const allowlist = new Set(["my-plugin.web"]);
|
|
219
276
|
for (const entry of getRegisteredToolsets()) {
|
|
220
277
|
if (allowlist.has(entry.spec.id)) {
|
|
@@ -282,7 +339,9 @@ ids with a stable namespace (<product-family>.<subset>, e.g. "foo.web").
|
|
|
282
339
|
|
|
283
340
|
- **Registration:** `defineToolset` stores the spec and handle in a global registry (shared across module instances, so multiple extensions see the same toolsets).
|
|
284
341
|
- **Persistence:** each toolset writes `{ enabled }` entries under its `persistKey` on the session branch. On `session_start` or `session_tree`, the library re-reads the branch and applies the last persisted state.
|
|
285
|
-
- **Default resolution:** a
|
|
342
|
+
- **Default resolution:** a `toolset-resolution-mode` entry on the branch controls how toolsets with no persisted state resolve on restore — `exclusion` (on/off by `defaultEnabled`), `inclusion` (deprecated unbounded floor), or `allowlist` (a finite branch-persisted array whose complement is computed at restore). Set by `setDefaultResolutionMode`, persists across reloads.
|
|
343
|
+
- **Defaults tiers:** each toolset's restore default resolves chat-branch entry → `toolsetDefaults` settings pin → packaged `spec.defaultEnabled`, filtered by resolution mode for unpinned toolsets only. Settings pins are read fresh from disk on each restore.
|
|
344
|
+
- **Null-tombstone-aware restore:** a `null` last branch entry (written by `clearToolsetEntry`) falls through to the settings tier instead of any stale prior entry; mode resolution is likewise null-tombstone-aware (`branchMode ?? "exclusion"`). Tombstones aren't sticky — a later toggle supersedes them.
|
|
286
345
|
- **Events:** a live toggle emits only when state actually changes (no-op toggles are suppressed); restore always emits, so side-effect owners stay in sync across reloads and tree navigations.
|
|
287
346
|
|
|
288
347
|
---
|