pi-hide-providers 0.1.20 → 0.1.22

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 CHANGED
@@ -179,3 +179,16 @@ npm run lint:dead # Dead code detection (knip)
179
179
  ## License
180
180
 
181
181
  MIT
182
+
183
+ ## Pi 1.0 compatibility (0.1.22)
184
+
185
+ Tested against Pi **1.0.0** with exact SDK development pins and wildcard host peers.
186
+ Native ModelRuntime filtering, scoped fallbacks and selector restoration are verified in both host layouts.
187
+
188
+ Run `bun run test:host` for the offline real-host regression. Set `PI1_HOST_PACKAGE` to an installed Pi package directory and `PI1_HOST_ENTRY=bundle` to test its bundled CLI runtime.
189
+
190
+ ## Pi 0.99 compatibility (0.1.21)
191
+
192
+ Filters the actual 0.99 ModelRuntime and scoped-selector fallback entries. Auth/cancellation arguments are forwarded; filtering composes with model sorting and restores on shutdown. Custom screens propagate IME focus.
193
+
194
+ Tested with Pi 0.99.0. Host-provided Pi packages are wildcard peers, not bundled dependencies; development uses exact 0.99.0 versions.
package/hide-providers.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { getAgentDir, CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
2
+ import { getAgentDir, CONFIG_DIR_NAME, ModelSelectorComponent } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  HIDE_COMMAND_DESCRIPTION,
5
5
  CONFIG_FILENAME,
@@ -76,16 +76,39 @@ function writeConfig(cwd: string, config: HideProvidersConfig): string {
76
76
  return path;
77
77
  }
78
78
 
79
+ import { installMethodPatch } from "./src/method-patch.js";
80
+
79
81
  // Extension
80
82
 
81
83
  export default function (pi: ExtensionAPI) {
82
84
  let currentRules: HideRule[] = [];
85
+ let activeRegistry: PatchedRegistry | undefined;
86
+ let restoreSelector: (() => void) | undefined;
87
+ pi.on("session_shutdown", () => {
88
+ if (activeRegistry) unpatchRegistry(activeRegistry);
89
+ activeRegistry = undefined;
90
+ restoreSelector?.();
91
+ restoreSelector = undefined;
92
+ });
83
93
 
84
94
  pi.on("session_start", async (_event, ctx) => {
85
95
  const config = readConfig(ctx.cwd);
86
96
  currentRules = config.hide;
87
97
 
88
98
  const registry = ctx.modelRegistry as unknown as PatchedRegistry;
99
+ if (activeRegistry && activeRegistry !== registry) unpatchRegistry(activeRegistry);
100
+ activeRegistry = registry;
101
+ if (ctx.mode === "tui" && !restoreSelector) {
102
+ restoreSelector = installMethodPatch(ModelSelectorComponent.prototype, "loadModelsFromSnapshot", function (original, ...args) {
103
+ // Pi keeps unavailable scoped models as fallback items. Filter that
104
+ // input as well as the runtime snapshot, without destroying the scope.
105
+ const scoped = this.scopedModels;
106
+ this.scopedModels = scoped.filter((item: { model: { provider: string; id: string } }) =>
107
+ !isHidden(currentRules, item.model.provider, item.model.id));
108
+ try { return original.apply(this, args); }
109
+ finally { this.scopedModels = scoped; }
110
+ });
111
+ }
89
112
  if (currentRules.length > 0) {
90
113
  patchRegistry(registry, () => currentRules);
91
114
  } else {
@@ -353,6 +376,8 @@ async function showHideSelector(
353
376
  );
354
377
 
355
378
  return {
379
+ get focused() { return selector.focused; },
380
+ set focused(value: boolean) { selector.focused = value; },
356
381
  render(width: number) {
357
382
  return selector.render(width);
358
383
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hide-providers",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Hide providers and models from pi's model selector — filter the /model list and Ctrl+P cycling via a configurable blocklist",
5
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
@@ -31,6 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "scripts": {
34
+ "test:host": "node scripts/pi1-host.mjs",
34
35
  "test": "vitest run",
35
36
  "test:watch": "vitest",
36
37
  "test:coverage": "vitest run --coverage",
@@ -38,8 +39,8 @@
38
39
  "lint:dead": "knip --no-gitignore"
39
40
  },
40
41
  "devDependencies": {
41
- "@earendil-works/pi-coding-agent": "0.87.0",
42
- "@earendil-works/pi-tui": "0.87.0",
42
+ "@earendil-works/pi-coding-agent": "1.0.0",
43
+ "@earendil-works/pi-tui": "1.0.0",
43
44
  "@types/node": "25.9.1",
44
45
  "@vitest/coverage-v8": "4.1.7",
45
46
  "knip": "6.14.1",
@@ -50,5 +51,9 @@
50
51
  "extensions": [
51
52
  "./hide-providers.ts"
52
53
  ]
54
+ },
55
+ "peerDependencies": {
56
+ "@earendil-works/pi-coding-agent": "*",
57
+ "@earendil-works/pi-tui": "*"
53
58
  }
54
59
  }
@@ -0,0 +1,22 @@
1
+ // Cooperating model extensions may unload in either order.
2
+ const PATCH = Symbol.for("pi.model-accessor.patch.v1");
3
+ type Method = (this: any, ...args: any[]) => any;
4
+ type Wrapped = Method & { [PATCH]?: { active: boolean; original: Method } };
5
+ function unwrap(method: Wrapped): Method {
6
+ while (method[PATCH] && !method[PATCH]!.active) method = method[PATCH]!.original;
7
+ return method;
8
+ }
9
+ export function installMethodPatch(target: any, name: string, handler: (this: any, original: Method, ...args: any[]) => any): () => void {
10
+ const original = target[name] as Method;
11
+ if (typeof original !== "function") throw new Error(`Pi model contract missing: ${name}`);
12
+ const state = { active: true, original };
13
+ const wrapped: Wrapped = function (...args) {
14
+ return state.active ? handler.call(this, original, ...args) : original.apply(this, args);
15
+ };
16
+ wrapped[PATCH] = state;
17
+ target[name] = wrapped;
18
+ return () => {
19
+ state.active = false;
20
+ if (target[name] === wrapped) target[name] = unwrap(original);
21
+ };
22
+ }
@@ -1,178 +1,68 @@
1
1
  import { isHidden, type HideRule } from "./index.js";
2
-
3
- interface ModelLike {
4
- provider: string;
5
- id: string;
6
- }
2
+ import { installMethodPatch } from "./method-patch.js";
7
3
 
8
4
  interface ModelRuntimeLike {
9
5
  getModels(providerId?: string): readonly unknown[];
10
6
  getAvailableSnapshot(): readonly unknown[];
11
- getAvailable?(providerId?: string): Promise<readonly unknown[]>;
7
+ getAvailable?(providerId?: string, options?: any): Promise<readonly unknown[]>;
12
8
  getModel(provider: string, modelId: string): unknown | undefined;
13
- __hide_providers_runtime_patched?: boolean;
14
- __hide_providers_get_rules?: () => HideRule[];
15
- __hide_providers_orig_getModels?: (providerId?: string) => readonly unknown[];
16
- __hide_providers_orig_getAvailableSnapshot?: () => readonly unknown[];
17
- __hide_providers_orig_getAvailable?: (providerId?: string) => Promise<readonly unknown[]>;
18
- __hide_providers_orig_getModel?: (provider: string, modelId: string) => unknown | undefined;
19
9
  }
20
-
21
10
  export interface PatchedRegistry {
22
- __hide_providers_patched?: boolean;
23
11
  runtime?: ModelRuntimeLike;
24
12
  getAvailable(): unknown[];
25
13
  getAll(): unknown[];
26
14
  find(provider: string, modelId: string): unknown | undefined;
27
- __hide_providers_get_rules?: () => HideRule[];
28
- __hide_providers_orig_getAvailable?: () => unknown[];
29
- __hide_providers_orig_getAll?: () => unknown[];
30
- __hide_providers_orig_find?: (provider: string, modelId: string) => unknown | undefined;
31
15
  }
32
-
33
- function filterModels(models: readonly unknown[], rules: ReadonlyArray<HideRule>): unknown[] {
16
+ interface PatchState {
17
+ getRules: () => HideRule[];
18
+ all: () => readonly unknown[];
19
+ restores: Array<() => void>;
20
+ }
21
+ const patches = new WeakMap<object, PatchState>();
22
+ function target(registry: PatchedRegistry) { return registry.runtime ?? registry; }
23
+ function filter(models: readonly unknown[], rules: HideRule[]): unknown[] {
34
24
  return models.filter((model) => {
35
- const candidate = model as ModelLike;
36
- return !isHidden(rules, candidate.provider, candidate.id);
25
+ const { provider, id } = model as { provider: string; id: string };
26
+ return !isHidden(rules, provider, id);
37
27
  });
38
28
  }
39
-
40
- function getRuntime(registry: PatchedRegistry): ModelRuntimeLike | undefined {
29
+ export function patchRegistry(registry: PatchedRegistry, getRules: () => HideRule[]): void {
30
+ const object = target(registry);
31
+ const existing = patches.get(object);
32
+ if (existing) { existing.getRules = getRules; return; }
41
33
  const runtime = registry.runtime;
42
- return runtime &&
43
- typeof runtime.getModels === "function" &&
44
- typeof runtime.getAvailableSnapshot === "function" &&
45
- typeof runtime.getModel === "function"
46
- ? runtime
47
- : undefined;
48
- }
49
-
50
- function patchRuntime(runtime: ModelRuntimeLike, getRules: () => HideRule[]): void {
51
- if (runtime.__hide_providers_runtime_patched) {
52
- runtime.__hide_providers_get_rules = getRules;
53
- return;
54
- }
55
-
56
- runtime.__hide_providers_runtime_patched = true;
57
- runtime.__hide_providers_get_rules = getRules;
58
- runtime.__hide_providers_orig_getModels = runtime.getModels.bind(runtime);
59
- runtime.__hide_providers_orig_getAvailableSnapshot = runtime.getAvailableSnapshot.bind(runtime);
60
- runtime.__hide_providers_orig_getModel = runtime.getModel.bind(runtime);
61
- if (runtime.getAvailable) {
62
- runtime.__hide_providers_orig_getAvailable = runtime.getAvailable.bind(runtime);
63
- }
64
-
65
- runtime.getModels = function (this: ModelRuntimeLike, providerId?: string) {
66
- return filterModels(
67
- this.__hide_providers_orig_getModels!(providerId),
68
- this.__hide_providers_get_rules!(),
69
- );
70
- };
71
- runtime.getAvailableSnapshot = function (this: ModelRuntimeLike) {
72
- return filterModels(
73
- this.__hide_providers_orig_getAvailableSnapshot!(),
74
- this.__hide_providers_get_rules!(),
75
- );
76
- };
77
- if (runtime.__hide_providers_orig_getAvailable) {
78
- runtime.getAvailable = async function (this: ModelRuntimeLike, providerId?: string) {
79
- return filterModels(
80
- await this.__hide_providers_orig_getAvailable!(providerId),
81
- this.__hide_providers_get_rules!(),
82
- );
83
- };
84
- }
85
- runtime.getModel = function (this: ModelRuntimeLike, provider: string, modelId: string) {
86
- if (isHidden(this.__hide_providers_get_rules!(), provider, modelId)) return undefined;
87
- return this.__hide_providers_orig_getModel!(provider, modelId);
34
+ const state: PatchState = {
35
+ getRules,
36
+ all: runtime ? runtime.getModels.bind(runtime) : registry.getAll.bind(registry),
37
+ restores: [],
88
38
  };
89
- }
90
-
91
- function unpatchRuntime(runtime: ModelRuntimeLike): void {
92
- if (!runtime.__hide_providers_runtime_patched) return;
93
-
94
- runtime.getModels = runtime.__hide_providers_orig_getModels!;
95
- runtime.getAvailableSnapshot = runtime.__hide_providers_orig_getAvailableSnapshot!;
96
- runtime.getModel = runtime.__hide_providers_orig_getModel!;
97
- if (runtime.__hide_providers_orig_getAvailable) {
98
- runtime.getAvailable = runtime.__hide_providers_orig_getAvailable;
39
+ try {
40
+ for (const name of runtime ? ["getModels", "getAvailableSnapshot"] : ["getAll", "getAvailable"]) {
41
+ state.restores.push(installMethodPatch(object, name, function (original, ...args) {
42
+ return filter(original.apply(this, args), state.getRules());
43
+ }));
44
+ }
45
+ if (runtime?.getAvailable) state.restores.push(installMethodPatch(object, "getAvailable", async function (original, ...args) {
46
+ // Forward provider and AuthOperationOptions (including cancellation).
47
+ return filter(await original.apply(this, args), state.getRules());
48
+ }));
49
+ state.restores.push(installMethodPatch(object, runtime ? "getModel" : "find", function (original, provider, id, ...rest) {
50
+ return isHidden(state.getRules(), provider, id) ? undefined : original.call(this, provider, id, ...rest);
51
+ }));
52
+ patches.set(object, state);
53
+ } catch (error) {
54
+ for (const restore of state.restores.reverse()) restore();
55
+ throw error;
99
56
  }
100
-
101
- delete runtime.__hide_providers_runtime_patched;
102
- delete runtime.__hide_providers_get_rules;
103
- delete runtime.__hide_providers_orig_getModels;
104
- delete runtime.__hide_providers_orig_getAvailableSnapshot;
105
- delete runtime.__hide_providers_orig_getAvailable;
106
- delete runtime.__hide_providers_orig_getModel;
107
57
  }
108
-
109
- export function patchRegistry(registry: PatchedRegistry, getRules: () => HideRule[]): void {
110
- const runtime = getRuntime(registry);
111
- if (runtime) patchRuntime(runtime, getRules);
112
-
113
- if (registry.__hide_providers_patched) {
114
- registry.__hide_providers_get_rules = getRules;
115
- return;
116
- }
117
-
118
- // Current pi internals read ModelRuntime directly. Patching its compatibility
119
- // ModelRegistry facade too would only double-filter results. Older pi versions
120
- // do not expose the runtime, so retain the original registry patch as fallback.
121
- if (runtime) return;
122
-
123
- registry.__hide_providers_patched = true;
124
- registry.__hide_providers_get_rules = getRules;
125
- registry.__hide_providers_orig_getAvailable = registry.getAvailable.bind(registry);
126
- registry.__hide_providers_orig_getAll = registry.getAll.bind(registry);
127
- registry.__hide_providers_orig_find = registry.find.bind(registry);
128
-
129
- registry.getAvailable = function (this: PatchedRegistry) {
130
- return filterModels(
131
- this.__hide_providers_orig_getAvailable!(),
132
- this.__hide_providers_get_rules!(),
133
- );
134
- };
135
- registry.getAll = function (this: PatchedRegistry) {
136
- return filterModels(
137
- this.__hide_providers_orig_getAll!(),
138
- this.__hide_providers_get_rules!(),
139
- );
140
- };
141
- registry.find = function (this: PatchedRegistry, provider: string, modelId: string) {
142
- if (isHidden(this.__hide_providers_get_rules!(), provider, modelId)) return undefined;
143
- return this.__hide_providers_orig_find!(provider, modelId);
144
- };
145
- }
146
-
147
58
  export function unpatchRegistry(registry: PatchedRegistry): void {
148
- const runtime = getRuntime(registry);
149
- if (runtime) unpatchRuntime(runtime);
150
-
151
- if (!registry.__hide_providers_patched) return;
152
- registry.getAvailable = registry.__hide_providers_orig_getAvailable!;
153
- registry.getAll = registry.__hide_providers_orig_getAll!;
154
- registry.find = registry.__hide_providers_orig_find!;
155
-
156
- delete registry.__hide_providers_patched;
157
- delete registry.__hide_providers_get_rules;
158
- delete registry.__hide_providers_orig_getAvailable;
159
- delete registry.__hide_providers_orig_getAll;
160
- delete registry.__hide_providers_orig_find;
59
+ const object = target(registry);
60
+ const state = patches.get(object);
61
+ if (!state) return;
62
+ for (const restore of state.restores.reverse()) restore();
63
+ patches.delete(object);
161
64
  }
162
-
163
- export function isRegistryPatched(registry: PatchedRegistry): boolean {
164
- return Boolean(
165
- registry.__hide_providers_patched || getRuntime(registry)?.__hide_providers_runtime_patched,
166
- );
167
- }
168
-
65
+ export function isRegistryPatched(registry: PatchedRegistry): boolean { return patches.has(target(registry)); }
169
66
  export function getUnfilteredModels(registry: PatchedRegistry): unknown[] {
170
- const runtime = getRuntime(registry);
171
- if (runtime?.__hide_providers_orig_getModels) {
172
- return [...runtime.__hide_providers_orig_getModels()];
173
- }
174
- if (registry.__hide_providers_orig_getAll) {
175
- return registry.__hide_providers_orig_getAll();
176
- }
177
- return registry.getAll();
67
+ return [...(patches.get(target(registry))?.all() ?? registry.getAll())];
178
68
  }