@gonrocca/zero-pi 0.1.11 → 0.1.12

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.
@@ -1,195 +1,195 @@
1
- // zero-pi — metered-provider guard, pi wiring.
2
- //
3
- // A thin pi extension that wires the pure decision logic in `provider-guard.ts`
4
- // to the `model_select` hook. Whenever pi reports a model switch, this handler
5
- // classifies it (in the pure module) and executes the resulting `GuardAction`:
6
- // a silent no-op for subscription providers, a non-blocking `warning` for a
7
- // metered provider with no equivalent (or a session `restore`), or — for a
8
- // deliberate switch to a metered provider that has an equivalent — a `confirm`
9
- // dialog offering to redirect to the subscription provider.
10
- //
11
- // All decisions live in `provider-guard.ts`; this file only declares the
12
- // minimal pi-API surface it uses, hooks `model_select`, and performs the I/O
13
- // (`confirm`/`notify`/`setModel`). Both `register` and the handler are wrapped
14
- // in a swallowing `try/catch` — exactly like `autotune-extension.ts`, a failure
15
- // must never break a pi session. The package stays dependency-free: only
16
- // `provider-guard.ts` is imported (no `node:*` is needed here, no third party).
17
-
18
- import {
19
- classifyModelSwitch,
20
- redirectFailedMessage,
21
- type ModelLike,
22
- } from "./provider-guard.ts";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Minimal local pi-API interfaces
26
- // ---------------------------------------------------------------------------
27
- //
28
- // `provider-guard-extension.ts` declares only the slice of pi's API it uses,
29
- // exactly like `autotune-extension.ts` and `zero-models.ts`. The guard never
30
- // depends on pi's full type surface — just the shapes below.
31
-
32
- /** The minimum a `Model` of pi must expose for the guard. Mirrors `ModelLike`
33
- * of the pure module — `setModel`/the registry both speak this shape. */
34
- interface PiModel {
35
- provider: string;
36
- id: string;
37
- }
38
-
39
- /** The model registry slice the guard uses: a `find` that resolves a model by
40
- * `(provider, id)` or returns `undefined` when no such model exists. */
41
- interface PiModelRegistry {
42
- find(provider: string, modelId: string): PiModel | undefined;
43
- }
44
-
45
- /** The slice of pi's UI surface the guard uses. `confirm` may be sync or async
46
- * (pi versions differ); both are handled uniformly with `await`. */
47
- interface PiUI {
48
- confirm(title: string, message: string): Promise<boolean> | boolean;
49
- notify(message: string, type?: "info" | "warning" | "error"): void;
50
- }
51
-
52
- /** The context pi passes to a `model_select` handler. `modelRegistry` is
53
- * optional — if absent the guard degrades to warn-only (it can no longer
54
- * resolve equivalents) rather than breaking. */
55
- interface PiModelSelectContext {
56
- ui: PiUI;
57
- modelRegistry?: PiModelRegistry;
58
- }
59
-
60
- /** The `model_select` event pi emits on a model change. The guard only reads
61
- * `model` and `source`; `source` is `set`/`cycle`/`restore` or, defensively,
62
- * any other string a future pi version might report. */
63
- interface PiModelSelectEvent {
64
- type: "model_select";
65
- model?: PiModel;
66
- source?: "set" | "cycle" | "restore" | string;
67
- }
68
-
69
- /** The slice of pi's extension API the guard uses: `on` to hook events and
70
- * `setModel` to apply a redirection. */
71
- interface PiExtensionAPI {
72
- on(
73
- event: string,
74
- handler: (event: PiModelSelectEvent, ctx: PiModelSelectContext) => void | Promise<void>,
75
- ): void;
76
- setModel(model: PiModel): Promise<boolean>;
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Handler
81
- // ---------------------------------------------------------------------------
82
-
83
- /**
84
- * The `model_select` guard handler.
85
- *
86
- * Runs entirely inside `register`'s swallowing `try/catch`, but is also wrapped
87
- * again here so no failure of `find`/`confirm`/`notify`/`setModel` can ever
88
- * propagate out of a pi session. The flow:
89
- *
90
- * 1. Validate `event`/`event.model` — a malformed event returns cleanly.
91
- * 2. Build the injected `lookup` over `ctx.modelRegistry.find`, wrapped in a
92
- * `try/catch` that returns `undefined`. If `modelRegistry` is absent,
93
- * degrade to an always-`undefined` lookup (warn-only).
94
- * 3. `classifyModelSwitch(model, source, lookup)` → a `GuardAction`.
95
- * 4. Execute the action: `ignore` → no-op; `warn` → `notify(..., "warning")`;
96
- * `offer-redirect` → `confirm`, and on a truthy answer `setModel` +
97
- * `notify(..., "info")` (or the failure notice if `setModel` returns
98
- * `false`). A falsy `confirm` leaves the user on the metered provider.
99
- */
100
- async function handleModelSelect(
101
- event: PiModelSelectEvent,
102
- ctx: PiModelSelectContext,
103
- pi: PiExtensionAPI,
104
- ): Promise<void> {
105
- try {
106
- // 1. Malformed event — no event, no model, or no UI to talk to: bail out.
107
- if (!event || !event.model || !ctx || !ctx.ui || typeof ctx.ui.notify !== "function") {
108
- return;
109
- }
110
-
111
- // 2. Build the injected lookup. `classifyModelSwitch` assumes a total
112
- // lookup, so `find` is wrapped in a `try/catch` returning `undefined`.
113
- // If `modelRegistry` is absent the guard cannot resolve equivalents —
114
- // degrade to an always-`undefined` lookup so a metered provider still
115
- // produces a warning rather than breaking.
116
- const registry = ctx.modelRegistry;
117
- const lookup = (provider: string, id: string): ModelLike | undefined => {
118
- if (!registry || typeof registry.find !== "function") return undefined;
119
- try {
120
- return registry.find(provider, id);
121
- } catch {
122
- // A registry lookup failure degrades to "no equivalent", never throws.
123
- return undefined;
124
- }
125
- };
126
-
127
- // 3. Classify the switch in the pure module.
128
- const action = classifyModelSwitch(event.model, event.source, lookup);
129
-
130
- // 4. Execute the action.
131
- if (action.kind === "ignore") {
132
- // Non-metered provider or malformed event — nothing to do.
133
- return;
134
- }
135
-
136
- if (action.kind === "warn") {
137
- // Metered provider with no equivalent, or a session `restore` — a single
138
- // non-blocking warning, no modal, no `setModel`.
139
- ctx.ui.notify(action.message, "warning");
140
- return;
141
- }
142
-
143
- // action.kind === "offer-redirect" — deliberate switch to a metered
144
- // provider that has a subscription equivalent. Offer the redirect; the
145
- // `confirm` dialog itself is the user's escape (AC 1.5).
146
- const accepted = await ctx.ui.confirm(action.confirmTitle, action.confirmMessage);
147
- if (!accepted) {
148
- // User declined or cancelled — leave them on the metered provider, no
149
- // second warning, no `setModel`.
150
- return;
151
- }
152
-
153
- const applied = await pi.setModel(action.safeModel);
154
- if (applied) {
155
- // Redirection took effect — confirm it with an `info` notification.
156
- ctx.ui.notify(action.redirectMessage, "info");
157
- } else {
158
- // `setModel` returned `false`: the switch did not apply. Do not claim
159
- // "redirected" — emit the optional failure notice instead.
160
- ctx.ui.notify(
161
- redirectFailedMessage(action.safeModel.id, action.safeModel.provider),
162
- "warning",
163
- );
164
- }
165
- } catch {
166
- // Any failure of the guard must never break a pi session.
167
- }
168
- }
169
-
170
- // ---------------------------------------------------------------------------
171
- // Registration
172
- // ---------------------------------------------------------------------------
173
-
174
- /**
175
- * The pi extension entry point.
176
- *
177
- * pi calls this once when the extension loads. It wires the guard handler to
178
- * the `model_select` event. The whole body is wrapped in a swallowing
179
- * `try/catch`, and the handler is wrapped again, so neither registration nor a
180
- * later failure can ever break a pi session. Called with no or an invalid
181
- * `pi` (missing, or no `.on` function), it no-ops cleanly.
182
- */
183
- export default function register(pi?: unknown): void {
184
- try {
185
- if (!pi || typeof (pi as PiExtensionAPI).on !== "function") {
186
- return;
187
- }
188
- const api = pi as PiExtensionAPI;
189
- api.on("model_select", (event: PiModelSelectEvent, ctx: PiModelSelectContext): Promise<void> => {
190
- return handleModelSelect(event, ctx, api);
191
- });
192
- } catch {
193
- // Registration itself must never break a pi session.
194
- }
195
- }
1
+ // zero-pi — metered-provider guard, pi wiring.
2
+ //
3
+ // A thin pi extension that wires the pure decision logic in `provider-guard.ts`
4
+ // to the `model_select` hook. Whenever pi reports a model switch, this handler
5
+ // classifies it (in the pure module) and executes the resulting `GuardAction`:
6
+ // a silent no-op for subscription providers, a non-blocking `warning` for a
7
+ // metered provider with no equivalent (or a session `restore`), or — for a
8
+ // deliberate switch to a metered provider that has an equivalent — a `confirm`
9
+ // dialog offering to redirect to the subscription provider.
10
+ //
11
+ // All decisions live in `provider-guard.ts`; this file only declares the
12
+ // minimal pi-API surface it uses, hooks `model_select`, and performs the I/O
13
+ // (`confirm`/`notify`/`setModel`). Both `register` and the handler are wrapped
14
+ // in a swallowing `try/catch` — exactly like `autotune-extension.ts`, a failure
15
+ // must never break a pi session. The package stays dependency-free: only
16
+ // `provider-guard.ts` is imported (no `node:*` is needed here, no third party).
17
+
18
+ import {
19
+ classifyModelSwitch,
20
+ redirectFailedMessage,
21
+ type ModelLike,
22
+ } from "./provider-guard.ts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Minimal local pi-API interfaces
26
+ // ---------------------------------------------------------------------------
27
+ //
28
+ // `provider-guard-extension.ts` declares only the slice of pi's API it uses,
29
+ // exactly like `autotune-extension.ts` and `zero-models.ts`. The guard never
30
+ // depends on pi's full type surface — just the shapes below.
31
+
32
+ /** The minimum a `Model` of pi must expose for the guard. Mirrors `ModelLike`
33
+ * of the pure module — `setModel`/the registry both speak this shape. */
34
+ interface PiModel {
35
+ provider: string;
36
+ id: string;
37
+ }
38
+
39
+ /** The model registry slice the guard uses: a `find` that resolves a model by
40
+ * `(provider, id)` or returns `undefined` when no such model exists. */
41
+ interface PiModelRegistry {
42
+ find(provider: string, modelId: string): PiModel | undefined;
43
+ }
44
+
45
+ /** The slice of pi's UI surface the guard uses. `confirm` may be sync or async
46
+ * (pi versions differ); both are handled uniformly with `await`. */
47
+ interface PiUI {
48
+ confirm(title: string, message: string): Promise<boolean> | boolean;
49
+ notify(message: string, type?: "info" | "warning" | "error"): void;
50
+ }
51
+
52
+ /** The context pi passes to a `model_select` handler. `modelRegistry` is
53
+ * optional — if absent the guard degrades to warn-only (it can no longer
54
+ * resolve equivalents) rather than breaking. */
55
+ interface PiModelSelectContext {
56
+ ui: PiUI;
57
+ modelRegistry?: PiModelRegistry;
58
+ }
59
+
60
+ /** The `model_select` event pi emits on a model change. The guard only reads
61
+ * `model` and `source`; `source` is `set`/`cycle`/`restore` or, defensively,
62
+ * any other string a future pi version might report. */
63
+ interface PiModelSelectEvent {
64
+ type: "model_select";
65
+ model?: PiModel;
66
+ source?: "set" | "cycle" | "restore" | string;
67
+ }
68
+
69
+ /** The slice of pi's extension API the guard uses: `on` to hook events and
70
+ * `setModel` to apply a redirection. */
71
+ interface PiExtensionAPI {
72
+ on(
73
+ event: string,
74
+ handler: (event: PiModelSelectEvent, ctx: PiModelSelectContext) => void | Promise<void>,
75
+ ): void;
76
+ setModel(model: PiModel): Promise<boolean>;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Handler
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * The `model_select` guard handler.
85
+ *
86
+ * Runs entirely inside `register`'s swallowing `try/catch`, but is also wrapped
87
+ * again here so no failure of `find`/`confirm`/`notify`/`setModel` can ever
88
+ * propagate out of a pi session. The flow:
89
+ *
90
+ * 1. Validate `event`/`event.model` — a malformed event returns cleanly.
91
+ * 2. Build the injected `lookup` over `ctx.modelRegistry.find`, wrapped in a
92
+ * `try/catch` that returns `undefined`. If `modelRegistry` is absent,
93
+ * degrade to an always-`undefined` lookup (warn-only).
94
+ * 3. `classifyModelSwitch(model, source, lookup)` → a `GuardAction`.
95
+ * 4. Execute the action: `ignore` → no-op; `warn` → `notify(..., "warning")`;
96
+ * `offer-redirect` → `confirm`, and on a truthy answer `setModel` +
97
+ * `notify(..., "info")` (or the failure notice if `setModel` returns
98
+ * `false`). A falsy `confirm` leaves the user on the metered provider.
99
+ */
100
+ async function handleModelSelect(
101
+ event: PiModelSelectEvent,
102
+ ctx: PiModelSelectContext,
103
+ pi: PiExtensionAPI,
104
+ ): Promise<void> {
105
+ try {
106
+ // 1. Malformed event — no event, no model, or no UI to talk to: bail out.
107
+ if (!event || !event.model || !ctx || !ctx.ui || typeof ctx.ui.notify !== "function") {
108
+ return;
109
+ }
110
+
111
+ // 2. Build the injected lookup. `classifyModelSwitch` assumes a total
112
+ // lookup, so `find` is wrapped in a `try/catch` returning `undefined`.
113
+ // If `modelRegistry` is absent the guard cannot resolve equivalents —
114
+ // degrade to an always-`undefined` lookup so a metered provider still
115
+ // produces a warning rather than breaking.
116
+ const registry = ctx.modelRegistry;
117
+ const lookup = (provider: string, id: string): ModelLike | undefined => {
118
+ if (!registry || typeof registry.find !== "function") return undefined;
119
+ try {
120
+ return registry.find(provider, id);
121
+ } catch {
122
+ // A registry lookup failure degrades to "no equivalent", never throws.
123
+ return undefined;
124
+ }
125
+ };
126
+
127
+ // 3. Classify the switch in the pure module.
128
+ const action = classifyModelSwitch(event.model, event.source, lookup);
129
+
130
+ // 4. Execute the action.
131
+ if (action.kind === "ignore") {
132
+ // Non-metered provider or malformed event — nothing to do.
133
+ return;
134
+ }
135
+
136
+ if (action.kind === "warn") {
137
+ // Metered provider with no equivalent, or a session `restore` — a single
138
+ // non-blocking warning, no modal, no `setModel`.
139
+ ctx.ui.notify(action.message, "warning");
140
+ return;
141
+ }
142
+
143
+ // action.kind === "offer-redirect" — deliberate switch to a metered
144
+ // provider that has a subscription equivalent. Offer the redirect; the
145
+ // `confirm` dialog itself is the user's escape (AC 1.5).
146
+ const accepted = await ctx.ui.confirm(action.confirmTitle, action.confirmMessage);
147
+ if (!accepted) {
148
+ // User declined or cancelled — leave them on the metered provider, no
149
+ // second warning, no `setModel`.
150
+ return;
151
+ }
152
+
153
+ const applied = await pi.setModel(action.safeModel);
154
+ if (applied) {
155
+ // Redirection took effect — confirm it with an `info` notification.
156
+ ctx.ui.notify(action.redirectMessage, "info");
157
+ } else {
158
+ // `setModel` returned `false`: the switch did not apply. Do not claim
159
+ // "redirected" — emit the optional failure notice instead.
160
+ ctx.ui.notify(
161
+ redirectFailedMessage(action.safeModel.id, action.safeModel.provider),
162
+ "warning",
163
+ );
164
+ }
165
+ } catch {
166
+ // Any failure of the guard must never break a pi session.
167
+ }
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Registration
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * The pi extension entry point.
176
+ *
177
+ * pi calls this once when the extension loads. It wires the guard handler to
178
+ * the `model_select` event. The whole body is wrapped in a swallowing
179
+ * `try/catch`, and the handler is wrapped again, so neither registration nor a
180
+ * later failure can ever break a pi session. Called with no or an invalid
181
+ * `pi` (missing, or no `.on` function), it no-ops cleanly.
182
+ */
183
+ export default function register(pi?: unknown): void {
184
+ try {
185
+ if (!pi || typeof (pi as PiExtensionAPI).on !== "function") {
186
+ return;
187
+ }
188
+ const api = pi as PiExtensionAPI;
189
+ api.on("model_select", (event: PiModelSelectEvent, ctx: PiModelSelectContext): Promise<void> => {
190
+ return handleModelSelect(event, ctx, api);
191
+ });
192
+ } catch {
193
+ // Registration itself must never break a pi session.
194
+ }
195
+ }