@aeondave/pi-skill-optimizer 3.0.0 → 3.0.1

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes are documented here.
4
4
 
5
+ ## 3.0.1 - 2026-09-30
6
+
7
+ ### Fixed
8
+
9
+ - Keep Pi 0.99's initially inactive `deferred` and `codemode` tools discoverable through the optimizer's `tool_search`. Activate only matching search results, retain them across later replanning, and leave initially unavailable direct or hidden tools outside the catalog.
10
+ - Reject blank tool searches without activating fallback results. Document how to disable Pi's replaceable built-in `tool-search` at the same scope as the optimizer, avoiding its startup conflict warning.
11
+
5
12
  ## 3.0.0 - 2026-09-28
6
13
 
7
14
  ### Scoped distribution - 2026-09-29
package/README.md CHANGED
@@ -26,12 +26,19 @@ Restart Pi, then check that it loaded:
26
26
  /skill-optimizer
27
27
  ```
28
28
 
29
+ Pi 0.99.0+ also includes a built-in `tool-search` extension. This optimizer provides
30
+ its own `tool_search`, so disable the built-in under **Built-in extensions** in
31
+ `pi config` to avoid the startup conflict warning. Use the global setting for a
32
+ personal install, or `pi config -l` for a project-only install. The optimizer
33
+ continues to find registered `deferred` and `codemode` tools. See the
34
+ [tool-search configuration](docs/CONFIGURATION.md#replacing-the-built-in-tool-search).
35
+
29
36
  That's enough to start. Optionally run `/skill-optimizer init` to generate routing hints for your library. **Init uses your current Pi model and may incur cost**; normal discovery does not.
30
37
 
31
38
  | Need | Command |
32
39
  | --- | --- |
33
40
  | Install only in this project | `pi install -l npm:@aeondave/pi-skill-optimizer` |
34
- | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.0.0` |
41
+ | Pin the current release | `pi install npm:@aeondave/pi-skill-optimizer@3.0.1` |
35
42
  | Update an unpinned install | `pi update npm:@aeondave/pi-skill-optimizer` |
36
43
 
37
44
  Restart after updating. Pinned installs stay pinned until you explicitly install another version.
@@ -14,6 +14,12 @@ Exact skill loading accepts only registered names. Resource and location resolut
14
14
 
15
15
  `excludeSkills` removes names from discovery and search, not from explicit exact loading. It is a routing preference, not an access-control boundary. The three resolver tools return their content without another output-reduction pass, so full skill instructions and recovered originals remain readable.
16
16
 
17
+ ### Replacing the built-in tool search
18
+
19
+ The extension registers `tool_search` itself, so it replaces Pi's built-in `builtin:tool-search` extension (the built-in is marked replaceable and the tool name decides). While both are in the load set, Pi prints a startup warning that the built-in was not loaded. The optimizer indexes eligible registered tools, including ones it deferred from Pi's active set and Pi-registered `deferred` or `codemode` tools. Matching inactive results activate additively; unrelated fallback results remain inactive. Pi's built-in MCP extension can still activate `tool_search` by name when a deferred server connects.
20
+
21
+ To silence the warning, disable the built-in where the optimizer runs: toggle `tool-search` off under Built-in extensions in `pi config`, or set `"extensions": ["-builtin:tool-search"]` in the applicable `settings.json`. A personal install needs the global setting in `~/.pi/agent/settings.json`; a project-only install can use `.pi/settings.json`. For development with `pi -e ./src/index.ts`, set the toggle locally or globally. Re-enable the built-in if you remove the optimizer.
22
+
17
23
  ## Commands
18
24
 
19
25
  | Command | Purpose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeondave/pi-skill-optimizer",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Less repeated context for Pi: local skill routing, on-demand tools, and recoverable output reduction.",
@@ -80,6 +80,6 @@
80
80
  "extensions": [
81
81
  "./src/index.ts"
82
82
  ],
83
- "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.0.0/assets/banner.png"
83
+ "image": "https://raw.githubusercontent.com/AeonDave/pi-skill-optimizer/v3.0.1/assets/banner.png"
84
84
  }
85
85
  }
package/src/index.ts CHANGED
@@ -112,6 +112,14 @@ const MAX_SEEN_EVIDENCE = 4_096;
112
112
  const MAX_HISTORY_ARTIFACTS = 128;
113
113
  const PROTECTED_RUNTIME_TOOLS = ["skill_search", "tool_search", "retrieve_output"] as const;
114
114
 
115
+ function isHostSearchableTool(tool: object): boolean {
116
+ return "exposure" in tool && (tool.exposure === "deferred" || tool.exposure === "codemode");
117
+ }
118
+
119
+ function isHostHiddenTool(tool: object): boolean {
120
+ return "exposure" in tool && tool.exposure === "hidden";
121
+ }
122
+
115
123
  type ConfigRecord = ReturnType<typeof getConfig>;
116
124
 
117
125
  const EMPTY_STORED_PROFILE: StoredProfile = {
@@ -565,12 +573,20 @@ export default function skillOptimizer(pi: ExtensionAPI) {
565
573
  };
566
574
 
567
575
  const rebuildToolCatalog = (state: RuntimeSnapshot): ToolDiscoveryCatalog => {
576
+ const allTools = pi.getAllTools();
568
577
  permittedToolNames ??= new Set([
569
578
  ...pi.getActiveTools().map((name) => name.toLowerCase()),
570
579
  ...PROTECTED_RUNTIME_TOOLS,
571
580
  ]);
572
- const discoverable = pi.getAllTools().filter((tool) =>
573
- permittedToolNames?.has(tool.name.toLowerCase()),
581
+ for (const tool of allTools) {
582
+ if (!isHostHiddenTool(tool)) continue;
583
+ const name = tool.name.toLowerCase();
584
+ permittedToolNames.delete(name);
585
+ retainedToolNames.delete(name);
586
+ deactivatedToolNames.delete(name);
587
+ }
588
+ const discoverable = allTools.filter((tool) =>
589
+ !isHostHiddenTool(tool) && permittedToolNames?.has(tool.name.toLowerCase()),
574
590
  );
575
591
  toolCatalog = buildToolDiscoveryCatalog(discoverable, {
576
592
  protect: [...PROTECTED_RUNTIME_TOOLS, ...state.config.alwaysTools],
@@ -578,13 +594,25 @@ export default function skillOptimizer(pi: ExtensionAPI) {
578
594
  return toolCatalog;
579
595
  };
580
596
 
597
+ const buildToolSearchCatalog = (state: RuntimeSnapshot): ToolDiscoveryCatalog => {
598
+ rebuildToolCatalog(state);
599
+ return buildToolDiscoveryCatalog(pi.getAllTools().filter((tool) =>
600
+ !isHostHiddenTool(tool)
601
+ && (permittedToolNames?.has(tool.name.toLowerCase()) || isHostSearchableTool(tool)),
602
+ ), {
603
+ protect: [...PROTECTED_RUNTIME_TOOLS, ...state.config.alwaysTools],
604
+ });
605
+ };
606
+
581
607
  const activateAdditively = (names: readonly string[]): string[] => {
582
608
  const requested = new Set(names.map((name) => name.toLowerCase()));
583
609
  const current = new Set(pi.getActiveTools().map((name) => name.toLowerCase()));
584
610
  const added: string[] = [];
585
611
  for (const tool of pi.getAllTools()) {
586
- if (!permittedToolNames?.has(tool.name.toLowerCase())) continue;
612
+ if (isHostHiddenTool(tool)) continue;
613
+ if (!permittedToolNames?.has(tool.name.toLowerCase()) && !isHostSearchableTool(tool)) continue;
587
614
  if (!requested.has(tool.name.toLowerCase())) continue;
615
+ permittedToolNames?.add(tool.name.toLowerCase());
588
616
  retainedToolNames.add(tool.name.toLowerCase());
589
617
  if (current.has(tool.name.toLowerCase())) continue;
590
618
  current.add(tool.name.toLowerCase());
@@ -592,8 +620,8 @@ export default function skillOptimizer(pi: ExtensionAPI) {
592
620
  added.push(tool.name);
593
621
  }
594
622
  const ordered = pi.getAllTools()
595
- .map((tool) => tool.name)
596
- .filter((name) => permittedToolNames?.has(name.toLowerCase()) && current.has(name.toLowerCase()));
623
+ .filter((tool) => !isHostHiddenTool(tool) && current.has(tool.name.toLowerCase()))
624
+ .map((tool) => tool.name);
597
625
  pi.setActiveTools(ordered);
598
626
  return added;
599
627
  };
@@ -607,8 +635,8 @@ export default function skillOptimizer(pi: ExtensionAPI) {
607
635
  for (const name of deactivatedToolNames) active.add(name);
608
636
  pi.setActiveTools(
609
637
  pi.getAllTools()
610
- .map((tool) => tool.name)
611
- .filter((name) => active.has(name.toLowerCase())),
638
+ .filter((tool) => !isHostHiddenTool(tool) && active.has(tool.name.toLowerCase()))
639
+ .map((tool) => tool.name),
612
640
  );
613
641
  deactivatedToolNames.clear();
614
642
  toolCatalog = undefined;
@@ -658,12 +686,13 @@ export default function skillOptimizer(pi: ExtensionAPI) {
658
686
  pi.registerTool({
659
687
  name: "tool_search",
660
688
  label: "Tool Search",
661
- description: "Search inactive tools locally. Returned tools are activated additively without disabling current tools.",
689
+ description: "Search local tool definitions and activate matching inactive tools without disabling current tools.",
662
690
  promptSnippet: "Search and activate additional tools on demand",
663
691
  parameters: TOOL_SEARCH_PARAMS,
664
692
  async execute(_id, params, _signal, _update, ctx) {
693
+ if (!params.query.trim()) throw new TypeError("query must not be empty");
665
694
  const state = ensureSnapshot(ctx);
666
- const catalog = toolCatalog ?? rebuildToolCatalog(state);
695
+ const catalog = buildToolSearchCatalog(state);
667
696
  const page = searchToolDiscoveryCatalog(catalog, params.query, {
668
697
  cursor: params.cursor,
669
698
  pageSize: Math.min(
@@ -671,7 +700,9 @@ export default function skillOptimizer(pi: ExtensionAPI) {
671
700
  state.config.toolSearchPageSize,
672
701
  ),
673
702
  });
674
- const activated = activateAdditively(page.results.map((result) => result.name));
703
+ const activated = activateAdditively(page.results
704
+ .filter((result) => result.matchedTerms.length > 0)
705
+ .map((result) => result.name));
675
706
  return {
676
707
  content: [{ type: "text", text: JSON.stringify({ ...page, activated }) }],
677
708
  details: { activated, confidence: page.confidence, failOpen: page.failOpen },