pi-lean-dimension 0.3.1 → 0.3.3

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.
Files changed (32) hide show
  1. package/README.md +22 -0
  2. package/node_modules/pi-lean-portal/README.md +42 -2
  3. package/node_modules/pi-lean-portal/__tests__/browser-toggle-migration.test.ts +145 -0
  4. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +24 -0
  5. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  6. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  7. package/node_modules/pi-lean-portal/browser-toggle.ts +98 -10
  8. package/node_modules/pi-lean-portal/package.json +2 -2
  9. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +22 -0
  10. package/node_modules/pi-lean-search/index.ts +21 -1
  11. package/node_modules/pi-lean-search/package.json +4 -2
  12. package/node_modules/pi-tool-masking/LICENSE +21 -0
  13. package/node_modules/pi-tool-masking/README.md +301 -0
  14. package/node_modules/pi-tool-masking/index.ts +497 -0
  15. package/node_modules/pi-tool-masking/package.json +42 -0
  16. package/node_modules/playwright/lib/common/index.js +3 -6
  17. package/node_modules/playwright/lib/common/index.js.txt +2 -2
  18. package/node_modules/playwright/lib/transform/esmLoader.js +3 -6
  19. package/node_modules/playwright/lib/transform/esmLoader.js.txt +2 -2
  20. package/node_modules/playwright/package.json +2 -2
  21. package/node_modules/playwright-core/lib/coreBundle.js +1 -1
  22. package/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-By56iMx7.js → codeMirrorModule-rXmQmLUY.js} +1 -1
  23. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B-dXF5JN.js +181 -0
  24. package/node_modules/playwright-core/lib/vite/traceViewer/assets/{xtermModule-COQkjINf.js → xtermModule-BuZfJS5v.js} +1 -1
  25. package/node_modules/playwright-core/lib/vite/traceViewer/{index.Dl36UVQT.js → index.KZ4wOW1K.js} +1 -1
  26. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  27. package/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.D962mr9b.js → uiMode.Dzuouizj.js} +2 -2
  28. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  29. package/node_modules/playwright-core/package.json +1 -1
  30. package/node_modules/playwright-core/types/structs.d.ts +8 -3
  31. package/package.json +3 -3
  32. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +0 -181
@@ -0,0 +1,301 @@
1
+ # pi-tool-masking
2
+
3
+ A library for pi plugin developers that groups tools into toggleable **toolsets** with persistent state and cross-extension events — eliminating the boilerplate every pi extension repeats when it wants to let users disable tools cleanly.
4
+
5
+ `pi-tool-masking` is **not** a pi extension itself. It is a dependency that your extension imports. It owns the toggle logic, the session-restore path, and the event bus. Your extension owns the commands, the status bar, and the user-facing surfaces.
6
+
7
+ ---
8
+
9
+ ## Why use it?
10
+
11
+ Without `pi-tool-masking`, every pi extension that toggles tools reimplements the same pattern:
12
+
13
+ 1. Maintain a `Set` of active tool names.
14
+ 2. On enable, add members to `pi.setActiveTools()` and `pi.appendEntry()` a persist record.
15
+ 3. On disable, filter members *out* of `pi.getActiveTools()` and append a persist record.
16
+ 4. On `session_start` / `session_tree`, walk the branch for persisted entries and re-apply state.
17
+ 5. Emit events so side-effect owners (status bars, pickers) can re-render.
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).
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install pi-tool-masking
27
+ ```
28
+
29
+ Then import from the package name in your extension:
30
+
31
+ ```ts
32
+ import { defineToolset, TOOLSET_EVENTS } from "pi-tool-masking";
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Quick start
38
+
39
+ ```ts
40
+ import { defineToolset, TOOLSET_EVENTS } from "pi-tool-masking";
41
+ import type { ToolsetSpec } from "pi-tool-masking";
42
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
43
+
44
+ const WEB_SPEC: ToolsetSpec = {
45
+ id: "my-plugin.web",
46
+ names: new Set(["web-fetch", "web-snapshot"]),
47
+ persistKey: "toolset-state:my-plugin.web",
48
+ defaultEnabled: true,
49
+ };
50
+
51
+ export default function activate(pi: ExtensionAPI) {
52
+ const webToolset = defineToolset(pi, WEB_SPEC);
53
+
54
+ // React to state changes (e.g. update a status glyph)
55
+ pi.events.on(TOOLSET_EVENTS.changed, (event) => {
56
+ if (event.id === "my-plugin.web") {
57
+ pi.ui.setStatus("myPlugin", event.enabled ? "on" : "off");
58
+ }
59
+ });
60
+
61
+ // Register a command that toggles the toolset
62
+ pi.registerCommand("my-plugin", {
63
+ description: "Toggle web tools on/off",
64
+ handler: async (args) => {
65
+ if (args.trim() === "on") {
66
+ webToolset.enable(pi);
67
+ } else if (args.trim() === "off") {
68
+ webToolset.disable(pi);
69
+ }
70
+ },
71
+ });
72
+ }
73
+ ```
74
+
75
+ That's it. The toolset is registered, its members are managed, and state persists across reloads, resumes, and tree navigations — no `appendEntry` or `session_start` restore code required.
76
+
77
+ ---
78
+
79
+ ## API
80
+
81
+ ### `defineToolset(pi, spec)`
82
+
83
+ Register a toolset and receive a `Toolset` handle (`enable`, `disable`, `isEnabled`).
84
+
85
+ | Parameter | Type | Required |
86
+ |---|---|---|
87
+ | `pi` | `ExtensionAPI` | Yes — the pi extension API instance |
88
+ | `spec` | `ToolsetSpec` | Yes — the toolset definition |
89
+
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
+
92
+ ### `setDefaultResolutionMode(pi, mode)`
93
+
94
+ Switch how toolsets with no persisted state resolve on restore. Two modes:
95
+
96
+ | Mode | Behavior on restore (no persisted entry) |
97
+ |---|---|
98
+ | `"exclusion"` (default) | Toolsets default **on** if `defaultEnabled` is true, **off** otherwise |
99
+ | `"inclusion"` | All unknown toolsets default **off** — useful for focus-mode "only these tools" workflows |
100
+
101
+ ### `getDefaultResolutionMode()`
102
+
103
+ Read the current default resolution mode. Returns `"exclusion"` until a session restore loads the persisted mode.
104
+
105
+ ### `getRegisteredToolsets()`
106
+
107
+ Return a read-only snapshot of every registered toolset (`{ spec, toolset }`). No `pi` argument needed — pure registry read.
108
+
109
+ ### `TOOLSET_EVENTS`
110
+
111
+ | Event | When |
112
+ |---|---|
113
+ | `changed` | A toolset was toggled by a consumer |
114
+ | `restored` | A toolset's state was restored from persisted session state |
115
+
116
+ ### Types
117
+
118
+ | Type | Description |
119
+ |---|---|
120
+ | `ToolsetSpec` | Schema for defining a toolset (see below) |
121
+ | `Toolset` | Handle returned by `defineToolset` |
122
+ | `ToolsetChangedEvent` | Shape of events emitted by `TOOLSET_EVENTS` |
123
+ | `RegistryEntry` | `{ spec: ToolsetSpec; toolset: Toolset }` — a single registered toolset |
124
+ | `DefaultResolutionMode` | `"exclusion" \| "inclusion"` |
125
+
126
+ ---
127
+
128
+ ## ToolsetSpec fields
129
+
130
+ ```ts
131
+ interface ToolsetSpec {
132
+ /** Stable id, e.g. "my-plugin.web". Used in persist keys and event payloads. */
133
+ id: string;
134
+
135
+ /** Human-readable name. Optional — falls back to id. */
136
+ label?: string;
137
+
138
+ /** One-line description. Optional — omitted when absent. */
139
+ description?: string;
140
+
141
+ /** Tool names this toolset governs. */
142
+ names: Set<string>;
143
+
144
+ /** Persistence key, e.g. "toolset-state:my-plugin.web". */
145
+ persistKey: string;
146
+
147
+ /** Fallback when no branch entry exists. Default true. */
148
+ defaultEnabled?: boolean;
149
+
150
+ /** IDs of toolsets that must be enabled for this one. */
151
+ requires?: string[];
152
+
153
+ /** When true, toggles emit one event per member in addition to the group event. */
154
+ emitMemberEvents?: boolean;
155
+ }
156
+ ```
157
+
158
+ ### Key behaviors
159
+
160
+ - **`requires` cascade:** enabling a toolset automatically enables all its dependencies (recursively). Disabling a toolset automatically disables all dependents.
161
+ - **Cycle detection:** circular `requires` relationships throw at toggle time.
162
+ - **`emitMemberEvents`:** opt into per-member fan-out events so a per-tool UI updates without the manager re-deriving which members moved.
163
+
164
+ ---
165
+
166
+ ## Toolset handle
167
+
168
+ ```ts
169
+ interface Toolset {
170
+ enable(pi: ExtensionAPI): void; // Enable all members (+ cascade to deps)
171
+ disable(pi: ExtensionAPI): void; // Disable all members (+ cascade to dependents)
172
+ isEnabled(pi: ExtensionAPI): boolean; // Check if at least one member is active
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Event payload
179
+
180
+ ```ts
181
+ interface ToolsetChangedEvent {
182
+ id: string; // Toolset id, e.g. "my-plugin.web"
183
+ enabled: boolean; // New state
184
+ member?: string; // Present only when emitMemberEvents is on — the specific tool that changed
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Real-world patterns
191
+
192
+ ### Status bar sync
193
+
194
+ ```ts
195
+ pi.events.on(TOOLSET_EVENTS.changed, (event) => {
196
+ if (event.id === "my-plugin.web") {
197
+ renderGlyph(event.enabled);
198
+ }
199
+ });
200
+
201
+ // Also listen to 'restored' so status is correct after /reload
202
+ pi.events.on(TOOLSET_EVENTS.restored, (event) => {
203
+ if (event.id === "my-plugin.web") {
204
+ renderGlyph(event.enabled);
205
+ }
206
+ });
207
+ ```
208
+
209
+ ### Focus mode (inclusion resolution)
210
+
211
+ ```ts
212
+ import { setDefaultResolutionMode, getRegisteredToolsets } from "pi-tool-masking";
213
+
214
+ // Enter focus: set inclusion mode so unknown toolsets stay off
215
+ setDefaultResolutionMode(pi, "inclusion");
216
+
217
+ // Enable only the allowlisted toolsets
218
+ const allowlist = new Set(["my-plugin.web"]);
219
+ for (const entry of getRegisteredToolsets()) {
220
+ if (allowlist.has(entry.spec.id)) {
221
+ entry.toolset.enable(pi);
222
+ } else {
223
+ entry.toolset.disable(pi);
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Dependent toolsets
229
+
230
+ ```ts
231
+ // Web tools are on by default; learn tools depend on web
232
+ const webSpec: ToolsetSpec = {
233
+ id: "my-plugin.web",
234
+ names: new Set(["web-fetch", "web-snapshot"]),
235
+ persistKey: "toolset-state:my-plugin.web",
236
+ defaultEnabled: true,
237
+ };
238
+
239
+ const learnSpec: ToolsetSpec = {
240
+ id: "my-plugin.learn",
241
+ names: new Set(["web-learn"]),
242
+ persistKey: "toolset-state:my-plugin.learn",
243
+ defaultEnabled: false,
244
+ requires: ["my-plugin.web"], // learn can't be on unless web is on
245
+ };
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Toolset naming
251
+
252
+ `defineToolset` can't tell which extension is calling it — pi's `ExtensionAPI`
253
+ doesn't expose the caller — so error messages can't name the responsible
254
+ extension directly. The toolset id is the only traceability signal, which is
255
+ why a stable, attributable id convention matters.
256
+
257
+ ### Convention (recommended, not enforced)
258
+
259
+ Prefix toolset ids with a stable namespace: `<product-family>.<subset>`, e.g.
260
+ `my-plugin.web`. The family may span multiple npm packages, and nothing checks
261
+ that the prefix matches a real package — it's for human traceability in
262
+ `/tbox list` and collision errors, not verification.
263
+
264
+ ### Enforcement floor
265
+
266
+ `defineToolset` enforces one naming invariant: **no two toolsets may claim the
267
+ same tool name.** Overlap is essentially always an authoring mistake and throws
268
+ at load time:
269
+
270
+ ```
271
+ [pi-tool-masking] name overlap: toolset "foo.search" claims tools already
272
+ owned by another toolset:
273
+ - tool "x" already claimed by toolset "bar.web" (registered from
274
+ /home/u/.pi/.../bar/index.ts, source: bar)
275
+ Each tool may belong to only one toolset. Naming convention: prefix toolset
276
+ ids with a stable namespace (<product-family>.<subset>, e.g. "foo.web").
277
+ ```
278
+
279
+ ---
280
+
281
+ ## How it works (for the curious)
282
+
283
+ - **Registration:** `defineToolset` stores the spec and handle in a global registry (shared across module instances, so multiple extensions see the same toolsets).
284
+ - **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 single `toolset-resolution-mode` entry on the branch controls whether unknown toolsets default on or off. This is set by `setDefaultResolutionMode` and persists across reloads.
286
+ - **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
+
288
+ ---
289
+
290
+ ## Consumer examples
291
+
292
+ This package is used by:
293
+
294
+ - **[pi-lean-dimension](https://github.com/coreyryanhanson/pi-lean-dimension)** — browser automation and SearXNG search tools toggled via `/web on|off|learn` and `/searxng-status`
295
+ - **[pi-tbox](https://github.com/coreyryanhanson/pi-tbox)** — cross-extension tool manager that queries `getRegisteredToolsets()` for its `/tbox toggle` and `/tbox focus` commands
296
+
297
+ ---
298
+
299
+ ## License
300
+
301
+ MIT. See [LICENSE](./LICENSE).