pi-free 2.0.0 → 2.0.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/config.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * 2. ~/.pi/free.json
7
7
  *
8
8
  * All exported values are getter functions so that runtime changes
9
- * (e.g. after /{provider}-toggle or /free) are visible immediately.
9
+ * (e.g. after toggle-{provider}) are visible immediately.
10
10
  */
11
11
 
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -112,7 +112,11 @@ function ensureConfigFile(): void {
112
112
  export function loadConfigFile(): PiFreeConfig {
113
113
  try {
114
114
  return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as PiFreeConfig;
115
- } catch {
115
+ } catch (err) {
116
+ _logger.warn("Could not parse config file — returning empty config", {
117
+ path: CONFIG_PATH,
118
+ error: err instanceof Error ? err.message : String(err),
119
+ });
116
120
  return {};
117
121
  }
118
122
  }
@@ -250,10 +254,30 @@ export function getOpenrouterApiKey(): string | undefined {
250
254
  // Hidden models (re-reads config on every call)
251
255
  // =============================================================================
252
256
 
253
- export function applyHidden<T extends { id: string }>(models: T[]): T[] {
257
+ /**
258
+ * Apply hidden models filter with provider scoping.
259
+ * Hidden models can be specified as:
260
+ * - "model-id" (global, applies to all providers - deprecated)
261
+ * - "provider/model-id" (provider-specific, preferred)
262
+ */
263
+ export function applyHidden<T extends { id: string }>(
264
+ models: T[],
265
+ providerId?: string,
266
+ ): T[] {
254
267
  const hidden = new Set(loadConfigFile().hidden_models ?? []);
255
268
  if (hidden.size === 0) return models;
256
- return models.filter((m) => !hidden.has(m.id));
269
+
270
+ return models.filter((m) => {
271
+ // Check provider-scoped ID (preferred format: "provider/model-id")
272
+ if (providerId && hidden.has(`${providerId}/${m.id}`)) {
273
+ return false;
274
+ }
275
+ // Check global ID (legacy format, still supported for backward compat)
276
+ if (hidden.has(m.id)) {
277
+ return false;
278
+ }
279
+ return true;
280
+ });
257
281
  }
258
282
 
259
283
  // =============================================================================
package/constants.ts CHANGED
@@ -23,6 +23,7 @@ export const ALL_UNIQUE_PROVIDERS = [
23
23
  /** @deprecated Qwen free tier no longer available */
24
24
  PROVIDER_QWEN,
25
25
  PROVIDER_MODAL,
26
+ PROVIDER_OLLAMA,
26
27
  ] as const;
27
28
 
28
29
  // =============================================================================
package/index.ts CHANGED
@@ -1,186 +1,148 @@
1
- /**
2
- * Pi-Free Providers Index
3
- *
4
- * Provides free model filtering for ALL providers (built-in + extension)
5
- * plus unique free/paid providers not covered by pi's built-in providers.
6
- *
7
- * Unique providers:
8
- * - Kilo: OAuth-based free models
9
- * - Cline: Cline bot integration
10
- * - NVIDIA: NVIDIA NIM hosting (free tier available)
11
- * - Qwen: OAuth-based Qwen access
12
- * - Modal: Modal Labs hosting
13
- */
14
-
15
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
- import { createLogger } from "./lib/logger.ts";
17
- import {
18
- applyGlobalFilter,
19
- getGlobalFreeOnly,
20
- getProviderRegistry,
21
- isFreeModel,
22
- registerWithGlobalToggle,
23
- } from "./lib/registry.ts";
24
- // Import unique provider extensions (only providers NOT built into pi)
25
- import cline from "./providers/cline/cline.ts";
26
- import cloudflare from "./providers/cloudflare/cloudflare.ts";
27
- import kilo from "./providers/kilo/kilo.ts";
28
- import modal from "./providers/modal/modal.ts";
29
- import nvidia from "./providers/nvidia/nvidia.ts";
30
- import ollama from "./providers/ollama/ollama.ts";
31
- import qwen from "./providers/qwen/qwen.ts";
32
-
33
- const _logger = createLogger("pi-free");
34
-
35
- // =============================================================================
36
- // Global Commands
37
- // =============================================================================
38
-
39
- function setupGlobalCommands(pi: ExtensionAPI) {
40
- // /free - Global toggle for ALL providers
41
- pi.registerCommand("free", {
42
- description: "Toggle free-only mode for ALL providers (on/off/status)",
43
- handler: async (args, ctx) => {
44
- const arg = args.trim().toLowerCase();
45
- const registry = getProviderRegistry();
46
-
47
- if (arg === "on" || arg === "true" || arg === "yes") {
48
- applyGlobalFilter(pi, true);
49
- ctx.ui.notify(
50
- "✓ Free-only mode enabled - paid models hidden for all providers",
51
- "info",
52
- );
53
- } else if (arg === "off" || arg === "false" || arg === "no") {
54
- applyGlobalFilter(pi, false);
55
- ctx.ui.notify(
56
- "✓ Paid models enabled - all models visible for all providers",
57
- "info",
58
- );
59
- } else if (arg === "status" || arg === "" || !arg) {
60
- const available = await ctx.modelRegistry.getAvailable();
61
- const freeCount = available.filter(isFreeModel).length;
62
- const status = getGlobalFreeOnly() ? "enabled" : "disabled";
63
-
64
- // Count by provider
65
- const lines = [
66
- `Free-only mode: ${status}`,
67
- `${freeCount}/${available.length} models free`,
68
- "",
69
- ];
70
- for (const [id, entry] of registry) {
71
- const free = entry.stored.free.length;
72
- const all = entry.stored.all.length || free;
73
- lines.push(`${id}: ${free}/${all} free`);
74
- }
75
-
76
- ctx.ui.notify(lines.join("\n"), "info");
77
- } else {
78
- ctx.ui.notify("Usage: /free [on|off|status]", "warning");
79
- }
80
- },
81
- });
82
-
83
- // /free-providers - Show free model counts by provider
84
- pi.registerCommand("free-providers", {
85
- description: "Show free/paid model counts for all pi-free providers",
86
- handler: async (_args, ctx) => {
87
- const lines = ["📊 Pi-Free Providers:", ""];
88
- const registry = getProviderRegistry();
89
-
90
- // Providers known to not expose pricing via API (all models show as "free")
91
- // OpenRouter and OpenCode expose actual pricing
92
- const noPricingApi = new Set([
93
- "mistral",
94
- "xai",
95
- "huggingface",
96
- "groq",
97
- "cerebras",
98
- ]);
99
- // Freemium providers - all models share a free tier quota
100
- const freemiumProviders = new Set(["nvidia"]);
101
-
102
- for (const [id, entry] of registry) {
103
- const free = entry.stored.free.length;
104
- const all = entry.stored.all.length || free;
105
- const indicator = entry.hasKey ? "🔑" : "🆓";
106
- const paid = all - free;
107
-
108
- if (freemiumProviders.has(id)) {
109
- // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
110
- lines.push(`${indicator} ${id}: ${all} models (freemium)`);
111
- } else if (noPricingApi.has(id)) {
112
- // Provider doesn't expose pricing - can't determine free vs paid
113
- lines.push(
114
- `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
115
- );
116
- } else if (paid === 0 && free > 0) {
117
- // All models are actually free
118
- lines.push(`${indicator} ${id}: ${free} free models`);
119
- } else {
120
- // Mix of free and paid
121
- lines.push(
122
- `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
123
- );
124
- }
125
- }
126
-
127
- if (registry.size === 0) {
128
- lines.push("(No providers registered yet)");
129
- }
130
-
131
- ctx.ui.notify(lines.join("\n"), "info");
132
- },
133
- });
134
- }
135
-
136
- // =============================================================================
137
- // Main Entry Point
138
- // =============================================================================
139
-
140
- export default async function (pi: ExtensionAPI) {
141
- const globalFreeOnly = getGlobalFreeOnly();
142
- _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
143
-
144
- // Setup global commands first
145
- setupGlobalCommands(pi);
146
-
147
- // Load all unique providers
148
- // Each provider will register itself with the global toggle system
149
- await Promise.allSettled([
150
- cloudflare(pi),
151
- modal(pi),
152
- nvidia(pi),
153
- kilo(pi),
154
- ollama(pi),
155
- // Qwen is deprecated
156
- qwen(pi).catch((err) => {
157
- _logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
158
- }),
159
- cline(pi),
160
- ]);
161
-
162
- // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
163
- // These only activate if the user has configured API keys (OpenRouter works without key too)
164
- const { setupDynamicBuiltInProviders } = await import(
165
- "./providers/dynamic-built-in/index.ts"
166
- );
167
- await setupDynamicBuiltInProviders(pi);
168
-
169
- // Apply initial global filter if free-only mode is enabled
170
- if (globalFreeOnly) {
171
- _logger.info("[pi-free] Applying initial free-only filter");
172
- await applyGlobalFilter(pi, true);
173
- }
174
-
175
- const registry = getProviderRegistry();
176
- _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
177
- }
178
-
179
- // Re-export registry helpers so consumers don't need deep imports
180
- export {
181
- applyGlobalFilter,
182
- getGlobalFreeOnly,
183
- getProviderRegistry,
184
- isFreeModel,
185
- registerWithGlobalToggle,
186
- };
1
+ /**
2
+ * Pi-Free Providers Index
3
+ *
4
+ * Provides free model filtering for ALL providers (built-in + extension)
5
+ * plus unique free/paid providers not covered by pi's built-in providers.
6
+ *
7
+ * Unique providers:
8
+ * - Kilo: OAuth-based free models
9
+ * - Cline: Cline bot integration
10
+ * - NVIDIA: NVIDIA NIM hosting (free tier available)
11
+ * - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
12
+ * - Qwen: OAuth-based Qwen access (deprecated)
13
+ * - Modal: Modal Labs hosting
14
+ */
15
+
16
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
17
+ import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
18
+ import { createLogger } from "./lib/logger.ts";
19
+ import {
20
+ applyGlobalFilter,
21
+ getGlobalFreeOnly,
22
+ getProviderRegistry,
23
+ isFreeModel,
24
+ registerWithGlobalToggle,
25
+ } from "./lib/registry.ts";
26
+ // Import unique provider extensions (only providers NOT built into pi)
27
+ import cline from "./providers/cline/cline.ts";
28
+ import cloudflare from "./providers/cloudflare/cloudflare.ts";
29
+ import kilo from "./providers/kilo/kilo.ts";
30
+ import modal from "./providers/modal/modal.ts";
31
+ import nvidia from "./providers/nvidia/nvidia.ts";
32
+ import ollama from "./providers/ollama/ollama.ts";
33
+ import qwen from "./providers/qwen/qwen.ts";
34
+
35
+ const _logger = createLogger("pi-free");
36
+
37
+ // =============================================================================
38
+ // Global Commands
39
+ // =============================================================================
40
+
41
+ function setupGlobalCommands(pi: ExtensionAPI) {
42
+ // /free-providers - Show free model counts by provider
43
+ pi.registerCommand("free-providers", {
44
+ description: "Show free/paid model counts for all pi-free providers",
45
+ handler: async (_args, ctx) => {
46
+ const lines = ["📊 Pi-Free Providers:", ""];
47
+ const registry = getProviderRegistry();
48
+
49
+ // Providers known to not expose pricing via API (all models show as "free")
50
+ // OpenRouter and OpenCode expose actual pricing
51
+ const noPricingApi = new Set([
52
+ "mistral",
53
+ "xai",
54
+ "huggingface",
55
+ "groq",
56
+ "cerebras",
57
+ ]);
58
+ // Freemium providers - all models share a free tier quota
59
+ const freemiumProviders = new Set(["nvidia"]);
60
+
61
+ for (const [id, entry] of registry) {
62
+ const free = entry.stored.free.length;
63
+ const all = entry.stored.all.length || free;
64
+ const indicator = entry.hasKey ? "🔑" : "🆓";
65
+ const paid = all - free;
66
+
67
+ if (freemiumProviders.has(id)) {
68
+ // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
69
+ lines.push(`${indicator} ${id}: ${all} models (freemium)`);
70
+ } else if (noPricingApi.has(id)) {
71
+ // Provider doesn't expose pricing - can't determine free vs paid
72
+ lines.push(
73
+ `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
74
+ );
75
+ } else if (paid === 0 && free > 0) {
76
+ // All models are actually free
77
+ lines.push(`${indicator} ${id}: ${free} free models`);
78
+ } else {
79
+ // Mix of free and paid
80
+ lines.push(
81
+ `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
82
+ );
83
+ }
84
+ }
85
+
86
+ if (registry.size === 0) {
87
+ lines.push("(No providers registered yet)");
88
+ }
89
+
90
+ ctx.ui.notify(lines.join("\n"), "info");
91
+ },
92
+ });
93
+ }
94
+
95
+ // =============================================================================
96
+ // Main Entry Point
97
+ // =============================================================================
98
+
99
+ export default async function (pi: ExtensionAPI) {
100
+ const globalFreeOnly = getGlobalFreeOnly();
101
+ _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
102
+
103
+ // Setup global commands first
104
+ setupGlobalCommands(pi);
105
+
106
+ // Load all unique providers
107
+ // Each provider will register itself with the global toggle system
108
+ await Promise.allSettled([
109
+ cloudflare(pi),
110
+ modal(pi),
111
+ nvidia(pi),
112
+ kilo(pi),
113
+ ollama(pi),
114
+ // Qwen is deprecated
115
+ qwen(pi).catch((err) => {
116
+ _logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
117
+ }),
118
+ cline(pi),
119
+ ]);
120
+
121
+ // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
122
+ // These only activate if the user has configured API keys (OpenRouter works without key too)
123
+ const { setupDynamicBuiltInProviders } = await import(
124
+ "./providers/dynamic-built-in/index.ts"
125
+ );
126
+ await setupDynamicBuiltInProviders(pi);
127
+
128
+ // Setup toggles for pi's built-in providers (e.g., OpenCode)
129
+ setupBuiltInProviderToggles(pi);
130
+
131
+ // Apply initial global filter if free-only mode is enabled
132
+ if (globalFreeOnly) {
133
+ _logger.info("[pi-free] Applying initial free-only filter");
134
+ await applyGlobalFilter(pi, true);
135
+ }
136
+
137
+ const registry = getProviderRegistry();
138
+ _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
139
+ }
140
+
141
+ // Re-export registry helpers so consumers don't need deep imports
142
+ export {
143
+ applyGlobalFilter,
144
+ getGlobalFreeOnly,
145
+ getProviderRegistry,
146
+ isFreeModel,
147
+ registerWithGlobalToggle,
148
+ };
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Built-in Provider Toggle Support
3
+ *
4
+ * Captures pi's built-in providers after session start and enables
5
+ * free/paid toggling for them via the global registry.
6
+ *
7
+ * Currently supports:
8
+ * - opencode (OpenCode / Zen gateway)
9
+ * - openrouter (OpenRouter)
10
+ *
11
+ * Usage: /toggle-opencode
12
+ */
13
+
14
+ import type { Api, Model } from "@mariozechner/pi-ai";
15
+ import type {
16
+ ExtensionAPI,
17
+ ProviderModelConfig,
18
+ } from "@mariozechner/pi-coding-agent";
19
+ import { getOpencodeShowPaid, getOpenrouterShowPaid } from "../config.ts";
20
+ import { createLogger } from "./logger.ts";
21
+ import { isFreeModel, registerWithGlobalToggle } from "./registry.ts";
22
+ import { createToggleState } from "./toggle-state.ts";
23
+
24
+ const _logger = createLogger("built-in-toggle");
25
+
26
+ // =============================================================================
27
+ // Configuration
28
+ // =============================================================================
29
+
30
+ interface BuiltInToggleConfig {
31
+ id: string;
32
+ getShowPaid: () => boolean;
33
+ }
34
+
35
+ const BUILT_IN_TOGGLE_PROVIDERS: BuiltInToggleConfig[] = [
36
+ { id: "opencode", getShowPaid: getOpencodeShowPaid },
37
+ { id: "openrouter", getShowPaid: getOpenrouterShowPaid },
38
+ ];
39
+
40
+ // =============================================================================
41
+ // State
42
+ // =============================================================================
43
+
44
+ interface BuiltInProviderState {
45
+ stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] };
46
+ reRegister: (models: ProviderModelConfig[]) => void;
47
+ toggleState: ReturnType<typeof createToggleState<ProviderModelConfig>>;
48
+ }
49
+
50
+ const providerStates = new Map<string, BuiltInProviderState>();
51
+ let commandsRegistered = false;
52
+
53
+ // =============================================================================
54
+ // Setup
55
+ // =============================================================================
56
+
57
+ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
58
+ // Register toggle commands once (available even before models load)
59
+ if (!commandsRegistered) {
60
+ for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
61
+ registerToggleCommand(pi, config);
62
+ }
63
+ commandsRegistered = true;
64
+ }
65
+
66
+ // Capture built-in models on session start and apply initial filter
67
+ pi.on("session_start", async (_event, ctx) => {
68
+ const available = ctx.modelRegistry.getAvailable();
69
+
70
+ for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
71
+ if (providerStates.has(config.id)) {
72
+ // Already captured this session — skip to avoid re-registering
73
+ continue;
74
+ }
75
+
76
+ const providerModels = available.filter(
77
+ (m: Model<Api>) => m.provider === config.id,
78
+ );
79
+ if (providerModels.length === 0) continue;
80
+
81
+ const allModels = providerModels.map(modelToProviderConfig);
82
+ const freeModels = allModels.filter(isFreeModel);
83
+
84
+ const baseUrl = providerModels[0].baseUrl;
85
+ const api = providerModels[0].api;
86
+ const apiKeyEnv = getApiKeyEnvForProvider(config.id);
87
+
88
+ const reRegister = (models: ProviderModelConfig[]) => {
89
+ pi.registerProvider(config.id, {
90
+ baseUrl,
91
+ apiKey: apiKeyEnv,
92
+ api,
93
+ models,
94
+ });
95
+ };
96
+
97
+ const stored = { free: freeModels, all: allModels };
98
+ const toggleState = createToggleState<ProviderModelConfig>({
99
+ providerId: config.id,
100
+ initialShowPaid: config.getShowPaid(),
101
+ initialModels: stored,
102
+ });
103
+
104
+ providerStates.set(config.id, {
105
+ stored,
106
+ reRegister,
107
+ toggleState,
108
+ });
109
+
110
+ registerWithGlobalToggle(config.id, stored, reRegister, true);
111
+
112
+ _logger.info(
113
+ `[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
114
+ );
115
+
116
+ const applied = toggleState.applyCurrent(reRegister);
117
+ _logger.info(
118
+ `[built-in-toggle] ${config.id}: applied ${applied.mode} mode with ${applied.models.length} models`,
119
+ );
120
+ }
121
+ });
122
+ }
123
+
124
+ // =============================================================================
125
+ // Per-provider toggle command
126
+ // =============================================================================
127
+
128
+ function registerToggleCommand(
129
+ pi: ExtensionAPI,
130
+ config: BuiltInToggleConfig,
131
+ ): void {
132
+ const commandName = `toggle-${config.id}`;
133
+ pi.registerCommand(commandName, {
134
+ description: `Toggle free/paid ${config.id} models`,
135
+ handler: async (_args, ctx) => {
136
+ const state = providerStates.get(config.id);
137
+ if (!state) {
138
+ ctx.ui.notify(
139
+ `${config.id}: models not loaded yet. Start a session first.`,
140
+ "warning",
141
+ );
142
+ return;
143
+ }
144
+
145
+ const applied = state.toggleState.toggle(state.reRegister);
146
+
147
+ if (applied.mode === "all") {
148
+ ctx.ui.notify(
149
+ `${config.id}: showing all ${state.stored.all.length} models`,
150
+ "info",
151
+ );
152
+ } else {
153
+ ctx.ui.notify(
154
+ `${config.id}: showing ${state.stored.free.length} free models`,
155
+ "info",
156
+ );
157
+ }
158
+ },
159
+ });
160
+ }
161
+
162
+ // =============================================================================
163
+ // Helpers
164
+ // =============================================================================
165
+
166
+ function modelToProviderConfig(m: Model<Api>): ProviderModelConfig {
167
+ return {
168
+ id: m.id,
169
+ name: m.name,
170
+ api: m.api,
171
+ reasoning: m.reasoning,
172
+ input: m.input,
173
+ cost: m.cost,
174
+ contextWindow: m.contextWindow,
175
+ maxTokens: m.maxTokens,
176
+ headers: m.headers,
177
+ compat: (m as any).compat,
178
+ };
179
+ }
180
+
181
+ function getApiKeyEnvForProvider(providerId: string): string {
182
+ const envMap: Record<string, string> = {
183
+ opencode: "OPENCODE_API_KEY",
184
+ openrouter: "OPENROUTER_API_KEY",
185
+ };
186
+ return envMap[providerId] || `${providerId.toUpperCase()}_API_KEY`;
187
+ }
@@ -0,0 +1,86 @@
1
+ import { saveConfig } from "../config.ts";
2
+
3
+ export type ToggleMode = "free" | "all";
4
+
5
+ export interface ToggleModelStore<T> {
6
+ free: T[];
7
+ all: T[];
8
+ }
9
+
10
+ interface CreateToggleStateOptions<T> {
11
+ providerId: string;
12
+ initialShowPaid: boolean;
13
+ save?: typeof saveConfig;
14
+ initialModels?: ToggleModelStore<T>;
15
+ }
16
+
17
+ interface ToggleResult<T> {
18
+ mode: ToggleMode;
19
+ models: T[];
20
+ }
21
+
22
+ export function createToggleState<T>({
23
+ providerId,
24
+ initialShowPaid,
25
+ save = saveConfig,
26
+ initialModels,
27
+ }: CreateToggleStateOptions<T>) {
28
+ let stored: ToggleModelStore<T> = initialModels ?? { free: [], all: [] };
29
+ let currentMode: ToggleMode = initialShowPaid ? "all" : "free";
30
+
31
+ function resolveMode(mode: ToggleMode): ToggleResult<T> {
32
+ if (mode === "all") {
33
+ if (stored.all.length > 0) {
34
+ return { mode: "all", models: stored.all };
35
+ }
36
+ return { mode: "free", models: stored.free };
37
+ }
38
+
39
+ if (stored.free.length > 0) {
40
+ return { mode: "free", models: stored.free };
41
+ }
42
+ return { mode: "all", models: stored.all };
43
+ }
44
+
45
+ function persist(mode: ToggleMode): void {
46
+ save({ [`${providerId}_show_paid`]: mode === "all" });
47
+ }
48
+
49
+ function applyMode(
50
+ mode: ToggleMode,
51
+ apply?: (models: T[]) => void,
52
+ ): ToggleResult<T> {
53
+ const resolved = resolveMode(mode);
54
+ currentMode = resolved.mode;
55
+ if (apply) apply(resolved.models);
56
+ return resolved;
57
+ }
58
+
59
+ return {
60
+ setModels(next: ToggleModelStore<T>): ToggleModelStore<T> {
61
+ stored = next;
62
+ const resolved = resolveMode(currentMode);
63
+ currentMode = resolved.mode;
64
+ return stored;
65
+ },
66
+ getStored(): ToggleModelStore<T> {
67
+ return stored;
68
+ },
69
+ getCurrentMode(): ToggleMode {
70
+ return currentMode;
71
+ },
72
+ getCurrentModels(): T[] {
73
+ return resolveMode(currentMode).models;
74
+ },
75
+ applyCurrent(apply?: (models: T[]) => void): ToggleResult<T> {
76
+ return applyMode(currentMode, apply);
77
+ },
78
+ applyMode,
79
+ toggle(apply?: (models: T[]) => void): ToggleResult<T> {
80
+ const nextMode = currentMode === "all" ? "free" : "all";
81
+ const resolved = applyMode(nextMode, apply);
82
+ persist(resolved.mode);
83
+ return resolved;
84
+ },
85
+ };
86
+ }