pi-lean-dimension 0.3.0 → 0.3.2
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/node_modules/pi-lean-portal/README.md +4 -6
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +7 -7
- package/node_modules/pi-lean-portal/browser-toggle.ts +5 -5
- package/node_modules/pi-lean-portal/package.json +2 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +6 -6
- package/node_modules/pi-lean-search/index.ts +14 -14
- package/node_modules/pi-lean-search/package.json +4 -2
- package/node_modules/pi-tool-masking/LICENSE +661 -0
- package/node_modules/pi-tool-masking/README.md +270 -0
- package/node_modules/pi-tool-masking/index.ts +464 -0
- package/node_modules/pi-tool-masking/package.json +39 -0
- package/package.json +3 -3
|
@@ -0,0 +1,270 @@
|
|
|
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 peer 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
|
+
// Portal tools are on by default; learn tools depend on portal
|
|
232
|
+
const portalSpec: 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
|
+
## How it works (for the curious)
|
|
251
|
+
|
|
252
|
+
- **Registration:** `defineToolset` stores the spec and handle in a global registry (shared across module instances, so multiple extensions see the same toolsets).
|
|
253
|
+
- **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.
|
|
254
|
+
- **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.
|
|
255
|
+
- **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.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Consumer examples
|
|
260
|
+
|
|
261
|
+
This package is used by:
|
|
262
|
+
|
|
263
|
+
- **[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`
|
|
264
|
+
- **[pi-tbox](https://github.com/coreyryanhanson/pi-tbox)** — cross-extension tool manager that queries `getRegisteredToolsets()` for its `/tbox toggle` and `/tbox focus` commands
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
GNU Affero General Public License v3.0 (AGPL-3.0). See [LICENSE](./LICENSE).
|