pi-web-kit 0.2.4 → 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/CHANGELOG.md +40 -0
- package/README.md +107 -10
- package/SECURITY.md +24 -2
- package/extensions/index.ts +235 -72
- package/package.json +11 -8
- package/src/config.ts +4 -4
- package/src/http.ts +15 -5
- package/src/install-telemetry.ts +48 -40
- package/src/limits.ts +39 -0
- package/src/output.ts +140 -0
- package/src/providers/brave.ts +43 -16
- package/src/providers/exa.ts +28 -11
- package/src/providers/firecrawl.ts +15 -6
- package/src/providers/index.ts +0 -3
- package/src/providers/markdown-new.ts +17 -3
- package/src/providers/tinyfish.ts +137 -17
- package/src/types.ts +12 -3
- package/src/urls.ts +3 -3
- package/src/providers/exa-mcp.ts +0 -102
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Declare bounded structured results for all five research tools, plus `web` namespace discovery and behavior annotations. Codemode now receives typed objects without `JSON.parse`; direct JSON text remains available.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Project provider responses to public fields and mask credentials consistently across text, structured data, details and progress. Omit arbitrary metadata, Context7 rules and raw backend error bodies.
|
|
18
|
+
- Restrict Basic/Bearer masking to authorization-header values so ordinary authentication documentation remains readable, while short header credentials stay masked.
|
|
19
|
+
- Honor explicit default-tool exclusions during late registration and retain manual deactivation across reload instead of force-activating optional research tools.
|
|
20
|
+
- Activate newly available optional tools when credentials arrive on reload, including existing positive selections, without reactivating previously disabled tools. Remember only fixed tool names in non-model session metadata; older sessions without that history use a conservative first-reload fallback.
|
|
21
|
+
- Reject cancelled calls even when a provider converts cancellation into per-page failure data.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Update the shared Pi development and contract-test baseline to 1.1.0; require Node.js >=22.19.0 to match the host runtime. Pi remains a host-supplied peer dependency.
|
|
26
|
+
|
|
27
|
+
## [0.3.0] - 2026-08-31
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Share install telemetry mechanics through `@mocito/install-telemetry` while preserving Pi-specific settings and state paths.
|
|
32
|
+
- Treat `web_search.numResults` as a provider-agnostic desired limit, cap it to each provider's service constraints, and report requested, effective, returned, and omitted counts.
|
|
33
|
+
- Add provider-agnostic `contextTokens` and `purpose` search controls, preserve provider-native grounding content, and allocate one bounded context budget across ranked results.
|
|
34
|
+
- Use adaptive extraction: native Exa/Brave context is retained automatically, while TinyFish fetch and Firecrawl scrape-on-search run only when expanded context is requested.
|
|
35
|
+
- Expose documented TinyFish search/fetch controls and Brave LLM Context controls.
|
|
36
|
+
- Add provider-agnostic `maxAgeMs`, pass refresh/cache intent through to Exa, TinyFish, and Firecrawl, and request complete bounded pages from Exa fetch.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Let `enableInstallTelemetry: false` override an enabled `PI_TELEMETRY` environment flag.
|
|
41
|
+
- Paginate TinyFish searches instead of sending its ignored `limit` parameter.
|
|
42
|
+
- Send markdown.new's documented `retain_images` field and report its response metadata.
|
|
43
|
+
- Match TinyFish fetch responses and per-URL errors by canonical URL instead of response position.
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
- Remove the Exa MCP search/fetch surface and make the direct Exa API the default. Existing `exa_mcp` configurations must select `exa` and provide `EXA_API_KEY`.
|
|
48
|
+
|
|
9
49
|
## [0.2.4] - 2026-07-28
|
|
10
50
|
|
|
11
51
|
### Changed
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Give [Pi](https://pi.dev) current web knowledge, authoritative library docs, and
|
|
|
10
10
|
- **Use docs that match the task** — resolve libraries and retrieve focused, version-aware documentation with code examples.
|
|
11
11
|
- **Find proven implementation patterns** — search practical usage, setup, migrations, and error context across real code.
|
|
12
12
|
- **Spend context wisely** — compact search results, chunked page reads, bounded output, and fetch caching keep research useful without overwhelming the model.
|
|
13
|
+
- **Compose research in scripts** — typed results let codemode filter and join data without parsing model-facing text.
|
|
13
14
|
- **Choose your providers** — mix Exa, TinyFish, Brave, Firecrawl, markdown.new, Context7, and Exa Code based on coverage, cost, and credentials.
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
@@ -39,6 +40,7 @@ pi -e /path/to/pi-mono/packages/pi-web-kit --web-provider-fetch markdown_new --p
|
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
This is an npm-compatible TypeScript Pi package. Bun is not required.
|
|
43
|
+
Use Pi 1.1.0 or newer and Node.js >=22.19.0 for the structured tool contracts.
|
|
42
44
|
|
|
43
45
|
## Quick usage
|
|
44
46
|
|
|
@@ -70,11 +72,10 @@ Pi chooses `web_search` or `web_fetch` automatically when the request calls for
|
|
|
70
72
|
|
|
71
73
|
## Providers
|
|
72
74
|
|
|
73
|
-
Defaults: `provider_search = "
|
|
75
|
+
Defaults: `provider_search = "exa"`, `provider_fetch = "exa"`.
|
|
74
76
|
|
|
75
77
|
| Provider | Search | Fetch | Key |
|
|
76
78
|
|---|---:|---:|---|
|
|
77
|
-
| `exa_mcp` | yes | yes | optional `EXA_API_KEY` |
|
|
78
79
|
| `exa` | yes | yes | `EXA_API_KEY` |
|
|
79
80
|
| `tinyfish` | yes | yes | `TINYFISH_API_KEY` |
|
|
80
81
|
| `brave` | yes | no | `BRAVE_SEARCH_API_KEY` |
|
|
@@ -83,6 +84,18 @@ Defaults: `provider_search = "exa_mcp"`, `provider_fetch = "exa_mcp"`.
|
|
|
83
84
|
|
|
84
85
|
Tool schemas are tailored to the configured providers at startup/reload, so only supported provider-specific fields are exposed. Restart/reload Pi after changing provider config.
|
|
85
86
|
|
|
87
|
+
Provider-native efficiencies are used as follows:
|
|
88
|
+
|
|
89
|
+
| Service | Efficient path |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Exa API | Keep included highlights; request bounded text in the same search only when requested; batch `/contents` fetches with freshness controls. |
|
|
92
|
+
| TinyFish | Paginate only as needed; use dedicated filters; batch only enough top-result fetches to fill requested context; pass TTL and intent. |
|
|
93
|
+
| Brave | Return pre-extracted LLM Context in one search call with native URL, token, snippet, threshold, and Goggles controls. |
|
|
94
|
+
| Firecrawl | Return search metadata by default; use scrape-on-search only when requested; pass cache/main-content options. |
|
|
95
|
+
| markdown.new | Keep `method: auto` so native Markdown falls back to AI and browser rendering only as needed; keep images opt-in. |
|
|
96
|
+
| Context7 | Support `fast` mode to skip reranking and reduce latency. |
|
|
97
|
+
| Exa Code | Send the requested/dynamic context token target directly to Exa Code Context. |
|
|
98
|
+
|
|
86
99
|
## Configuration
|
|
87
100
|
|
|
88
101
|
Resolution order: defaults < environment variables < global config < trusted project config < CLI flags. Project config is ignored unless Pi trusts the current project, including in print, JSON, and RPC modes.
|
|
@@ -92,8 +105,8 @@ Resolution order: defaults < environment variables < global config < trusted pro
|
|
|
92
105
|
```bash
|
|
93
106
|
PI_OFFLINE=1 # disables install/update telemetry
|
|
94
107
|
PI_TELEMETRY=0 # disables install/update telemetry
|
|
95
|
-
PI_WEB_KIT_PROVIDER_SEARCH=
|
|
96
|
-
PI_WEB_KIT_PROVIDER_FETCH=
|
|
108
|
+
PI_WEB_KIT_PROVIDER_SEARCH=exa|tinyfish|brave|firecrawl
|
|
109
|
+
PI_WEB_KIT_PROVIDER_FETCH=exa|tinyfish|markdown_new|firecrawl
|
|
97
110
|
EXA_API_KEY=... # enables Exa provider and code_search
|
|
98
111
|
CONTEXT7_API_KEY=... # enables library_search and library_docs
|
|
99
112
|
TINYFISH_API_KEY=...
|
|
@@ -148,9 +161,15 @@ Searches with the active search provider and returns compact results grouped by
|
|
|
148
161
|
|---|---|---|
|
|
149
162
|
| `query` | string | Single search query. |
|
|
150
163
|
| `queries` | string[] | Multiple related search queries. Max 5 after de-duplication. |
|
|
151
|
-
| `numResults` | integer |
|
|
164
|
+
| `numResults` | integer | Desired results per query. Default: 10. Values above the active provider's limit are capped rather than rejected. |
|
|
165
|
+
| `contextTokens` | integer | Desired extracted context across the result set. Omitted native context uses an 8,192-token output budget; an explicit value can enable extraction. Any positive value; capped at 10,000. |
|
|
166
|
+
| `purpose` | string | Optional task/use-case hint for providers that support separate intent. |
|
|
167
|
+
|
|
168
|
+
`numResults` controls source breadth. `contextTokens` controls grounding depth. The tool automatically keeps native/included Exa highlights and Brave LLM Context. Explicit `contextTokens` enables extra extraction for Exa, TinyFish, and Firecrawl; this can add provider calls or provider cost. Search results keep a compact `snippet` plus ranked `content` and `contentFormat`, with one shared context budget and the existing 50KB tool-output limit.
|
|
169
|
+
|
|
170
|
+
Provider caps are Exa 100, Brave 50, and Firecrawl 100. TinyFish is paginated internally through its service maximum of page 10 and may make up to 11 search requests for one query. Search output reports requested, effective, returned, and omitted result/context counts. Brave `maxUrls` remains as a deprecated alias for `numResults`.
|
|
152
171
|
|
|
153
|
-
|
|
172
|
+
Other provider-specific parameters are exposed only for the configured provider. These include Exa date/domain filters; TinyFish domain, date, geography, language, and publication filters; Brave locale, freshness, spellcheck, Goggles, and LLM Context controls; and Firecrawl scrape/search options.
|
|
154
173
|
|
|
155
174
|
### `web_fetch`
|
|
156
175
|
|
|
@@ -163,8 +182,9 @@ Fetches page content with the active fetch provider. Results are cached in memor
|
|
|
163
182
|
| `offset` | integer | Character offset for cached/ranged reads. Single URL only. |
|
|
164
183
|
| `limit` | integer | Maximum characters to return. Default: 30,000 for one URL, 8,000 for multiple URLs. |
|
|
165
184
|
| `refresh` | boolean | Refetch even if cached. |
|
|
185
|
+
| `maxAgeMs` | integer | Desired maximum local/provider-cached page age in milliseconds. `0` requests live content where supported. |
|
|
166
186
|
|
|
167
|
-
Provider-specific parameters are exposed only for the configured provider
|
|
187
|
+
Provider-specific parameters are exposed only for the configured provider. TinyFish supports `purpose`, `format`, links/images, selectors, per-URL timeout, and a seconds-based `ttl` alias. Exa supports the hours-based `maxAgeHours` alias. markdown.new supports `method` / `retainImages`. Firecrawl supports `format`, `waitFor`, `mobile`, structured `location`, and its existing `maxAge` alias. `refresh: true` also requests live content from Exa, TinyFish, and Firecrawl instead of only bypassing the local cache.
|
|
168
188
|
|
|
169
189
|
### `library_search`
|
|
170
190
|
|
|
@@ -210,16 +230,93 @@ Finds practical code examples, implementation context, setup snippets, migration
|
|
|
210
230
|
| Max cached bytes | 20 MiB |
|
|
211
231
|
| Max URLs per call | 10 |
|
|
212
232
|
| Max queries per call | 5 |
|
|
213
|
-
|
|
|
233
|
+
| Provider `numResults` caps | Exa 100; Brave 50; Firecrawl 100 |
|
|
234
|
+
| Search context budget | 10,000 tokens |
|
|
214
235
|
| Max URL length | 2048 characters |
|
|
215
236
|
|
|
216
237
|
Cache keys include the provider, canonical URL, fetch-affecting parameters, relevant provider defaults, and an opaque SHA-256 API-key/account scope. Internal cache keys are never returned in tool output. `refresh: true` bypasses and replaces the cached entry.
|
|
217
238
|
|
|
239
|
+
## Structured results and discovery
|
|
240
|
+
|
|
241
|
+
All five tools declare `outputSchema` and return objects through `structuredContent`.
|
|
242
|
+
Codemode receives those objects directly; remove old `JSON.parse(await tools.…())`
|
|
243
|
+
wrappers. Direct calls still return useful JSON text with exactly the same
|
|
244
|
+
bounded, redacted value. Renderer `details` remain compact and are not the
|
|
245
|
+
script API.
|
|
246
|
+
|
|
247
|
+
| Tool | Script result |
|
|
248
|
+
| --- | --- |
|
|
249
|
+
| `web_search` | `{ provider, queries: [{ query, requestedResultLimit, effectiveResultLimit, requestedContextTokens?, effectiveContextTokens, contextCharacters, omittedContextCharacters?, resultCount, omittedResultCount?, results }] }`. Each result has `url` and optional `title`, `snippet`, `content`, `contentFormat`, `siteName`, `position`. |
|
|
250
|
+
| `web_fetch` | `{ provider, results }`. Each item is either `{ url, error }` or `{ url, fetchedUrl, title?, content, format, cached, refreshed, range }`. `range` has `offset`, `limit`, `returned`, `total`, `truncated`, `hasPrevious`, `hasNext`, and optional `nextOffset`. |
|
|
251
|
+
| `library_search` | `{ provider: "context7", libraryName, query, searchFilterApplied?, results }`. Each candidate has `id`; title, description, branch, dates, state, scores, counts, stars and versions are optional. |
|
|
252
|
+
| `library_docs` | `{ provider: "context7", libraryId, query, codeSnippets, infoSnippets }`. Code snippets expose optional title/description/language/ID/page/source/token fields and `codeList: [{ language, code }]`; info snippets require `content` with optional page/breadcrumb/token fields. |
|
|
253
|
+
| `code_search` | `{ provider: "exa", query, response, resultsCount?, searchTime?, outputTokens?, requestId? }`. Provider token counts are informational, not a second Pi usage report. |
|
|
254
|
+
|
|
255
|
+
Every result also allows the top-level fallback `{ truncated: true, message }`
|
|
256
|
+
when metadata alone cannot fit. Check this before reading ordinary fields.
|
|
257
|
+
Both text and structured payloads stay within the same 50,000-byte JSON budget.
|
|
258
|
+
Fetch slices retain range continuation; search results report omitted counts.
|
|
259
|
+
Missing/invalid optional provider fields are omitted. Invalid required fields
|
|
260
|
+
fail the call rather than yielding a guessed success object.
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
const search = await tools.web_search({ query: "Pi extension API", numResults: 3 });
|
|
264
|
+
if ("truncated" in search) throw new Error(search.message);
|
|
265
|
+
const urls = search.queries.flatMap(q => q.results.map(r => r.url));
|
|
266
|
+
if (urls.length) {
|
|
267
|
+
const pages = await tools.web_fetch({ urls: urls.slice(0, 10) });
|
|
268
|
+
if ("truncated" in pages) throw new Error(pages.message);
|
|
269
|
+
text(pages.results.filter(r => !("error" in r)).map(r => ({
|
|
270
|
+
url: r.url, excerpt: r.content.slice(0, 500), nextOffset: r.range.nextOffset
|
|
271
|
+
})));
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Validation, whole-request/provider, malformed-output and cancellation failures
|
|
276
|
+
throw: direct calls become Pi tool errors and scripts must use `try`/`catch` or
|
|
277
|
+
`Promise.allSettled`. Individual fetch failures are successful result data with
|
|
278
|
+
an `error` field, not `isError: true`. These tools do not return structured success
|
|
279
|
+
objects alongside `isError: true`. A trusted result hook can replace that contract;
|
|
280
|
+
annotations do not bypass hooks or approvals.
|
|
281
|
+
|
|
282
|
+
The namespace is `web`, not a name prefix: calls remain `tools.web_fetch(…)`.
|
|
283
|
+
`await describeNamespace("web")`, `await describeTool("web_fetch")`, and
|
|
284
|
+
`await searchTools("page content", { namespace: "web" })` work even with
|
|
285
|
+
`codemode.inlineBudget: 0`. Independent reads can run in parallel. The advisory
|
|
286
|
+
hints are read-only, non-destructive, idempotent, and open-world: repeated reads
|
|
287
|
+
can return different content and incur provider charges.
|
|
288
|
+
|
|
289
|
+
Direct exposure remains the default, and codemode is optional. No package-level
|
|
290
|
+
deferred/codemode exposure switch is added: it would make inactive direct tools
|
|
291
|
+
nested-callable and change the meaning of disabling them. Use Pi's `codemode.mode`
|
|
292
|
+
(`on` or `only`) and `inlineBudget` to reduce declarations while retaining the
|
|
293
|
+
active-tool boundary. Optional research tools still require their provider keys.
|
|
294
|
+
Explicit `defaultTools` `-name` entries are honored at late registration; reload
|
|
295
|
+
restores Pi's active selection rather than re-enabling manually disabled tools.
|
|
296
|
+
New optional tools activate when credentials first become available on reload,
|
|
297
|
+
including existing positive `defaultTools` selections. Previously registered
|
|
298
|
+
tools are not force-reactivated, even if settings still positively select them.
|
|
299
|
+
The extension records only previously registered tool names in branch-local
|
|
300
|
+
`pi-web-kit:registered-tools` session metadata, outside model context; it does
|
|
301
|
+
not store activation choices or credentials. The record is refreshed before
|
|
302
|
+
reload so navigating to older branch history cannot revive a manually disabled
|
|
303
|
+
tool. Older sessions without this metadata preserve inactivity on their first
|
|
304
|
+
reload, so they may require explicit activation or a restart.
|
|
305
|
+
Malformed/unknown-version metadata uses the same
|
|
306
|
+
conservative fallback.
|
|
307
|
+
CLI exclusions remain host-enforced.
|
|
308
|
+
|
|
218
309
|
## Privacy and security
|
|
219
310
|
|
|
220
311
|
`pi-web-kit` sends search queries and fetched URLs to the configured provider. Developer-search tools send library/doc queries to Context7 and code-context queries to Exa when those tools are enabled. Fetch providers may also receive provider-specific options. API keys are read from environment variables or local config files and are used only for provider requests.
|
|
221
312
|
|
|
222
|
-
The extension rejects non-HTTP(S) URLs and URLs with embedded username/password credentials.
|
|
313
|
+
The extension rejects non-HTTP(S) URLs and URLs with embedded username/password credentials.
|
|
314
|
+
Returned data is projected to declared public fields; raw provider metadata,
|
|
315
|
+
Context7 `rules`, cache keys and HTTP error bodies are not returned. Known API
|
|
316
|
+
keys, URL credential patterns and Basic/Bearer authorization-header values are masked in text, structured data,
|
|
317
|
+
renderer details and progress. This is not a general sensitive-data scanner:
|
|
318
|
+
page content remains untrusted, and Pi retains caller-supplied arguments in its
|
|
319
|
+
own transcript. Do not put credentials in queries or URLs.
|
|
223
320
|
|
|
224
321
|
Report security issues privately. See [SECURITY.md](SECURITY.md).
|
|
225
322
|
|
|
@@ -238,7 +335,7 @@ Report security issues privately. See [SECURITY.md](SECURITY.md).
|
|
|
238
335
|
|
|
239
336
|
Requirements:
|
|
240
337
|
|
|
241
|
-
- Node.js >=
|
|
338
|
+
- Node.js >= 22.19.0
|
|
242
339
|
- npm
|
|
243
340
|
|
|
244
341
|
Common commands:
|
package/SECURITY.md
CHANGED
|
@@ -21,8 +21,30 @@ The maintainer will acknowledge reports as soon as practical and coordinate disc
|
|
|
21
21
|
|
|
22
22
|
`pi-web-kit` is a Pi package. Pi extensions execute with the same permissions as the local user running Pi. Users should review installed Pi packages and only install packages from sources they trust.
|
|
23
23
|
|
|
24
|
-
`pi-web-kit` sends search queries and fetched URLs to the configured third-party provider. Page content returned by providers is cached in memory for the lifetime of the Pi process, subject to TTL and size limits. API keys are read from environment variables or local config files and are used only for provider requests. Do not commit API keys, tokens, or config files containing secrets.
|
|
24
|
+
`pi-web-kit` sends search queries and fetched URLs to the configured third-party provider. A TinyFish search may send the same query in up to 11 paginated requests to satisfy the requested result limit. When `contextTokens` is explicit, TinyFish also receives the selected result URLs for batched extraction, while Firecrawl may scrape search results. Page content returned by providers is cached in memory for the lifetime of the Pi process, subject to TTL and size limits. API keys are read from environment variables or local config files and are used only for provider requests. Do not commit API keys, tokens, or config files containing secrets.
|
|
25
25
|
|
|
26
|
-
On startup,
|
|
26
|
+
On startup, `@mocito/install-telemetry` sends a best-effort install/update telemetry ping to the configured telemetry endpoint once per package version unless Pi telemetry is disabled or offline mode is enabled. The ping includes only the package name, version, and parsed platform/runtime/architecture from its User-Agent; it does not include prompts, queries, fetched URLs, file paths, config values, or API keys.
|
|
27
27
|
|
|
28
28
|
The extension validates URLs before fetch calls and only accepts `http:` and `https:` URLs without embedded credentials. This validation reduces accidental misuse but does not sandbox provider responses or the local Pi process.
|
|
29
|
+
|
|
30
|
+
Tool results use an allowlisted output schema before publication. Model text and
|
|
31
|
+
`structuredContent` share the same 50,000-byte JSON budget and credential masking;
|
|
32
|
+
renderer details and progress also mask configured API keys, URL credential
|
|
33
|
+
patterns and Basic/Bearer authorization-header values (not ordinary auth prose).
|
|
34
|
+
Arbitrary provider metadata, Context7 `rules`, cache keys,
|
|
35
|
+
and HTTP failure bodies are excluded. Untrusted per-page error bodies become
|
|
36
|
+
generic diagnostics; controlled HTTP status/timeout messages remain useful.
|
|
37
|
+
This does not classify all private information in fetched content or sanitize
|
|
38
|
+
Pi's stored caller arguments. Do not send secrets in queries or URLs.
|
|
39
|
+
|
|
40
|
+
The `web` namespace and read-only/idempotent/open-world annotations are advisory.
|
|
41
|
+
They do not approve provider spending, bypass tool hooks, or make an inactive
|
|
42
|
+
direct tool reachable from nested dispatch. Project trust, URL validation,
|
|
43
|
+
provider request limits and cancellation still apply.
|
|
44
|
+
|
|
45
|
+
Branch-local `pi-web-kit:registered-tools` custom entries remember only this
|
|
46
|
+
package's fixed tool names as they first become available. They contain no
|
|
47
|
+
credentials, configuration values, activation decisions or usage data and do
|
|
48
|
+
not enter model context. This distinguishes a newly available optional tool
|
|
49
|
+
from re-registration of a manually disabled tool; Pi still owns activation
|
|
50
|
+
and exclusions. Missing or invalid history does not force tools active on reload.
|