@aliou/pi-neuralwatt 0.7.6 → 0.8.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.
Files changed (37) hide show
  1. package/README.md +4 -2
  2. package/{src/extensions → extensions}/command-quotas/command.ts +2 -2
  3. package/extensions/command-quotas/components/progress-bar.ts +38 -0
  4. package/{src/extensions → extensions}/command-quotas/components/quota-tabs.ts +4 -4
  5. package/{src/extensions → extensions}/command-quotas/components/quotas-display.ts +1 -1
  6. package/{src/extensions → extensions}/command-quotas/index.ts +3 -3
  7. package/extensions/provider/commands/settings/index.ts +246 -0
  8. package/{src/extensions → extensions}/provider/index.ts +36 -25
  9. package/{src/extensions → extensions}/provider/models/hidden.ts +3 -3
  10. package/{src/extensions → extensions}/provider/quota-store.ts +4 -4
  11. package/{src/extensions → extensions}/provider/sse-quotas.ts +1 -1
  12. package/{src/extensions → extensions}/quota-warnings/index.ts +11 -20
  13. package/{src/extensions → extensions}/quota-warnings/notifier.ts +9 -8
  14. package/{src/extensions → extensions}/sub-bar-integration/index.ts +9 -10
  15. package/{src/extensions → extensions}/sub-bar-integration/snapshot.ts +3 -3
  16. package/package.json +20 -14
  17. package/schema.json +50 -6
  18. package/src/config/defaults.ts +17 -0
  19. package/src/config/index.ts +9 -0
  20. package/src/config/loader.ts +74 -0
  21. package/src/config/migration/01-disable-legacy-model-ids-by-default.ts +44 -0
  22. package/src/config/migration/02-flat-to-nested-config.ts +149 -0
  23. package/src/config/migration/index.ts +16 -0
  24. package/src/config/types.ts +77 -0
  25. package/src/{types/quota-events.ts → events.ts} +25 -13
  26. package/src/lib/neuralwatt-api.ts +1 -1
  27. package/src/types/quota-result.ts +12 -0
  28. package/src/utils/quota-bar.ts +0 -38
  29. package/src/config.ts +0 -210
  30. /package/{src/lib/env.ts → extensions/_shared/auth.ts} +0 -0
  31. /package/{src/extensions → extensions}/provider/context-overflow.ts +0 -0
  32. /package/{src/extensions → extensions}/provider/models/cache.ts +0 -0
  33. /package/{src/extensions → extensions}/provider/models/index.ts +0 -0
  34. /package/{src/extensions → extensions}/provider/models/legacy.ts +0 -0
  35. /package/{src/extensions → extensions}/provider/models/public-models.ts +0 -0
  36. /package/{src/extensions → extensions}/provider/rate-limit-error.ts +0 -0
  37. /package/{src/extensions → extensions}/provider/stream-simple.ts +0 -0
package/README.md CHANGED
@@ -36,7 +36,7 @@ pi install npm:@aliou/pi-neuralwatt
36
36
  pi install git:github.com/aliou/pi-neuralwatt
37
37
 
38
38
  # Local development
39
- pi -e ./src/extensions/provider/index.ts
39
+ pi -e ./extensions/provider/index.ts
40
40
  ```
41
41
 
42
42
  ## Usage
@@ -80,9 +80,11 @@ Configure features with `/neuralwatt:settings`:
80
80
 
81
81
  The provider itself cannot be disabled — it is always loaded.
82
82
 
83
+ Configuration uses nested per-feature sections. Existing flat config files are migrated automatically, with a backup written next to the migrated config.
84
+
83
85
  ## Adding or Updating Models
84
86
 
85
- Models are hardcoded in `src/extensions/provider/models.ts` and validated against the live API. To update:
87
+ Models are hardcoded in `extensions/provider/models/public-models.ts` and validated against the live API. To update:
86
88
 
87
89
  1. Run `pnpm test` — it fetches `/v1/models` and compares against hardcoded definitions
88
90
  2. Fix any discrepancies (missing models, changed context windows)
@@ -3,8 +3,8 @@ import {
3
3
  type ExtensionAPI,
4
4
  getAgentDir,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { getNeuralwattApiKey } from "../../lib/env";
7
- import { fetchQuotas } from "../../lib/neuralwatt-api";
6
+ import { fetchQuotas } from "../../src/lib/neuralwatt-api";
7
+ import { getNeuralwattApiKey } from "../_shared/auth";
8
8
  import { QuotasComponent } from "./components/quotas-display";
9
9
 
10
10
  function missingAuthMessage(): string {
@@ -0,0 +1,38 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { Severity } from "../../../src/utils/quota-bar";
3
+
4
+ export type BarStyle = "filled-used" | "filled-remaining";
5
+
6
+ /**
7
+ * Render a progress bar.
8
+ *
9
+ * filled-used: filled region = used portion (colored by severity of remaining%)
10
+ * filled-remaining: filled region = remaining portion (colored by severity of remaining%)
11
+ */
12
+ export function renderProgressBar(
13
+ percent: number,
14
+ width: number,
15
+ theme: Theme,
16
+ severity: Severity,
17
+ style: BarStyle = "filled-remaining",
18
+ ): string {
19
+ const clamped = Math.max(0, Math.min(100, Math.round(percent)));
20
+ const filledCount = Math.round((clamped / 100) * width);
21
+
22
+ const parts: string[] = [];
23
+ for (let idx = 0; idx < width; idx++) {
24
+ const isFilled = idx < filledCount;
25
+ if (style === "filled-used") {
26
+ // filled = used (severity color), empty = remaining (dim)
27
+ parts.push(
28
+ isFilled ? theme.fg(severity, "\u2593") : theme.fg("success", "\u2591"),
29
+ );
30
+ } else {
31
+ // filled = remaining (severity color), empty = used (dim)
32
+ parts.push(
33
+ isFilled ? theme.fg(severity, "\u2588") : theme.fg("dim", "\u2591"),
34
+ );
35
+ }
36
+ }
37
+ return parts.join("");
38
+ }
@@ -1,17 +1,17 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import type { NeuralwattQuotas } from "../../../types/quota-api";
3
+ import type { NeuralwattQuotas } from "../../../src/types/quota-api";
4
4
  import {
5
5
  percentCreditsRemaining,
6
6
  percentEnergyRemaining,
7
- renderProgressBar,
8
7
  severityFromPercent,
9
- } from "../../../utils/quota-bar";
8
+ } from "../../../src/utils/quota-bar";
10
9
  import {
11
10
  formatKwh,
12
11
  formatTokens,
13
12
  formatUsd,
14
- } from "../../../utils/quota-format";
13
+ } from "../../../src/utils/quota-format";
14
+ import { renderProgressBar } from "./progress-bar";
15
15
 
16
16
  /**
17
17
  * Subscription tab — plan details, energy quota, billing period.
@@ -2,7 +2,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
3
  import type { Component, TUI } from "@earendil-works/pi-tui";
4
4
  import { Loader, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
5
- import type { NeuralwattQuotas } from "../../../types/quota-api";
5
+ import type { NeuralwattQuotas } from "../../../src/types/quota-api";
6
6
  import {
7
7
  renderCreditsTab,
8
8
  renderSubscriptionTab,
@@ -1,9 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { configLoader } from "../../src/config";
2
3
  import {
3
- configLoader,
4
4
  NEURALWATT_EXTENSIONS_REGISTER_EVENT,
5
5
  NEURALWATT_EXTENSIONS_REQUEST_EVENT,
6
- } from "../../config";
6
+ } from "../../src/events";
7
7
  import { registerQuotasCommand } from "./command";
8
8
 
9
9
  export default async function (pi: ExtensionAPI) {
@@ -11,7 +11,7 @@ export default async function (pi: ExtensionAPI) {
11
11
 
12
12
  const config = configLoader.getConfig();
13
13
 
14
- if (config.quotaCommand) {
14
+ if (config.quotaCommand.enabled) {
15
15
  registerQuotasCommand(pi);
16
16
  }
17
17
 
@@ -0,0 +1,246 @@
1
+ import {
2
+ registerSettingsCommand,
3
+ type SettingsSection,
4
+ } from "@aliou/pi-utils-settings";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import type { SettingItem } from "@earendil-works/pi-tui";
7
+ import {
8
+ configLoader,
9
+ type NeuralwattConfig,
10
+ type NeuralwattRawConfig,
11
+ type ResolvedNeuralwattConfig,
12
+ } from "../../../../src/config";
13
+ import {
14
+ NEURALWATT_CONFIG_UPDATED_EVENT,
15
+ type NeuralwattFeatureId,
16
+ } from "../../../../src/events";
17
+
18
+ export interface RegisterNeuralwattSettingsOptions {
19
+ getLoadedFeatures: () => Set<NeuralwattFeatureId>;
20
+ }
21
+
22
+ function emitConfigUpdated(pi: ExtensionAPI): void {
23
+ pi.events.emit(NEURALWATT_CONFIG_UPDATED_EVENT, {
24
+ config: configLoader.getConfig(),
25
+ });
26
+ }
27
+
28
+ function featureRow(
29
+ id: NeuralwattFeatureId,
30
+ label: string,
31
+ description: string,
32
+ configValue: boolean,
33
+ isLoaded: boolean,
34
+ ): SettingItem {
35
+ if (isLoaded) {
36
+ return {
37
+ id,
38
+ label,
39
+ description,
40
+ currentValue: configValue ? "enabled" : "disabled",
41
+ values: ["enabled", "disabled"],
42
+ };
43
+ }
44
+ return {
45
+ id,
46
+ label,
47
+ description: `${description} (Not loaded by Pi)`,
48
+ currentValue: "unavailable",
49
+ values: [],
50
+ };
51
+ }
52
+
53
+ function optionalFeatureValue(value: unknown): boolean | undefined {
54
+ if (typeof value === "boolean") return value;
55
+ if (value && typeof value === "object") {
56
+ const enabled = (value as { enabled?: boolean }).enabled;
57
+ if (typeof enabled === "boolean") return enabled;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ function featureValue(value: unknown, fallback: boolean): boolean {
63
+ return optionalFeatureValue(value) ?? fallback;
64
+ }
65
+
66
+ function toNestedConfig(config: NeuralwattRawConfig): NeuralwattConfig {
67
+ const provider = "provider" in config ? config.provider : undefined;
68
+
69
+ return {
70
+ provider: {
71
+ ...(provider ?? {}),
72
+ includeLegacyModelIds:
73
+ provider?.includeLegacyModelIds ??
74
+ ("includeLegacyModelIds" in config
75
+ ? config.includeLegacyModelIds
76
+ : undefined),
77
+ includeHiddenModels:
78
+ provider?.includeHiddenModels ??
79
+ ("includeHiddenModels" in config
80
+ ? config.includeHiddenModels
81
+ : undefined),
82
+ },
83
+ quotaCommand: {
84
+ ...(typeof config.quotaCommand === "object" ? config.quotaCommand : {}),
85
+ enabled: optionalFeatureValue(config.quotaCommand),
86
+ },
87
+ quotaWarnings: {
88
+ ...(typeof config.quotaWarnings === "object" ? config.quotaWarnings : {}),
89
+ enabled: optionalFeatureValue(config.quotaWarnings),
90
+ },
91
+ subBarIntegration: {
92
+ ...(typeof config.subBarIntegration === "object"
93
+ ? config.subBarIntegration
94
+ : {}),
95
+ enabled: optionalFeatureValue(config.subBarIntegration),
96
+ },
97
+ };
98
+ }
99
+
100
+ export function registerNeuralwattSettings(
101
+ pi: ExtensionAPI,
102
+ options: RegisterNeuralwattSettingsOptions,
103
+ ): void {
104
+ const { getLoadedFeatures } = options;
105
+
106
+ registerSettingsCommand<NeuralwattRawConfig, ResolvedNeuralwattConfig>(pi, {
107
+ commandName: "neuralwatt:settings",
108
+ title: "Neuralwatt Settings",
109
+ configStore: configLoader,
110
+ buildSections: (tabConfig, resolved): SettingsSection[] => {
111
+ const loaded = getLoadedFeatures();
112
+ return [
113
+ {
114
+ label: "Features",
115
+ items: [
116
+ featureRow(
117
+ "quotaCommand",
118
+ "Quota command",
119
+ "Toggle the /neuralwatt:quota command, showing your API usage at a glance",
120
+ featureValue(
121
+ tabConfig?.quotaCommand,
122
+ resolved.quotaCommand.enabled,
123
+ ),
124
+ loaded.has("quotaCommand"),
125
+ ),
126
+ featureRow(
127
+ "quotaWarnings",
128
+ "Quota warnings",
129
+ "Toggle notifications when credits or energy are running low",
130
+ featureValue(
131
+ tabConfig?.quotaWarnings,
132
+ resolved.quotaWarnings.enabled,
133
+ ),
134
+ loaded.has("quotaWarnings"),
135
+ ),
136
+ featureRow(
137
+ "subBarIntegration",
138
+ "Sub-bar integration",
139
+ "Toggle integration with the status bar and sub-core",
140
+ featureValue(
141
+ tabConfig?.subBarIntegration,
142
+ resolved.subBarIntegration.enabled,
143
+ ),
144
+ loaded.has("subBarIntegration"),
145
+ ),
146
+ ],
147
+ },
148
+ {
149
+ label: "Other settings",
150
+ items: [
151
+ {
152
+ id: "includeLegacyModelIds",
153
+ label: "Legacy model IDs",
154
+ description:
155
+ "Include deprecated Neuralwatt model IDs as aliases in the model picker",
156
+ currentValue:
157
+ ((tabConfig &&
158
+ "provider" in tabConfig &&
159
+ tabConfig.provider?.includeLegacyModelIds) ??
160
+ (tabConfig &&
161
+ "includeLegacyModelIds" in tabConfig &&
162
+ tabConfig.includeLegacyModelIds) ??
163
+ resolved.provider.includeLegacyModelIds)
164
+ ? "include"
165
+ : "ignore",
166
+ values: ["include", "ignore"],
167
+ },
168
+ {
169
+ id: "includeHiddenModels",
170
+ label: "Hidden models",
171
+ description:
172
+ "Include Neuralwatt models that are accessible via API key but not advertised in the public model list",
173
+ currentValue:
174
+ ((tabConfig &&
175
+ "provider" in tabConfig &&
176
+ tabConfig.provider?.includeHiddenModels) ??
177
+ (tabConfig &&
178
+ "includeHiddenModels" in tabConfig &&
179
+ tabConfig.includeHiddenModels) ??
180
+ resolved.provider.includeHiddenModels)
181
+ ? "include"
182
+ : "ignore",
183
+ values: ["include", "ignore"],
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+ },
189
+ onSettingChange: (id, newValue, config) => {
190
+ // Non-feature toggles are handled first so they are not blocked by the
191
+ // loaded-features guard (they are managed directly by the provider).
192
+ if (id === "includeLegacyModelIds") {
193
+ const nestedConfig = toNestedConfig(config);
194
+ return {
195
+ ...nestedConfig,
196
+ provider: {
197
+ ...nestedConfig.provider,
198
+ includeLegacyModelIds: newValue === "include",
199
+ },
200
+ };
201
+ }
202
+
203
+ if (id === "includeHiddenModels") {
204
+ const nestedConfig = toNestedConfig(config);
205
+ return {
206
+ ...nestedConfig,
207
+ provider: {
208
+ ...nestedConfig.provider,
209
+ includeHiddenModels: newValue === "include",
210
+ },
211
+ };
212
+ }
213
+
214
+ if (!getLoadedFeatures().has(id as NeuralwattFeatureId)) {
215
+ return null;
216
+ }
217
+
218
+ const enabled = newValue === "enabled";
219
+ switch (id) {
220
+ case "quotaCommand":
221
+ return {
222
+ ...toNestedConfig(config),
223
+ quotaCommand: { ...toNestedConfig(config).quotaCommand, enabled },
224
+ };
225
+ case "quotaWarnings":
226
+ return {
227
+ ...toNestedConfig(config),
228
+ quotaWarnings: { ...toNestedConfig(config).quotaWarnings, enabled },
229
+ };
230
+ case "subBarIntegration":
231
+ return {
232
+ ...toNestedConfig(config),
233
+ subBarIntegration: {
234
+ ...toNestedConfig(config).subBarIntegration,
235
+ enabled,
236
+ },
237
+ };
238
+ default:
239
+ return null;
240
+ }
241
+ },
242
+ onSave: async () => {
243
+ emitConfigUpdated(pi);
244
+ },
245
+ });
246
+ }
@@ -1,25 +1,22 @@
1
- import { getApiProvider } from "@earendil-works/pi-ai";
1
+ import { getApiProvider } from "@earendil-works/pi-ai/compat";
2
2
  import type {
3
3
  ExtensionAPI,
4
4
  ProviderModelConfig,
5
5
  } from "@earendil-works/pi-coding-agent";
6
+ import { configLoader } from "../../src/config";
6
7
  import {
7
- configLoader,
8
- emitConfigUpdated,
9
8
  NEURALWATT_CONFIG_UPDATED_EVENT,
10
9
  NEURALWATT_EXTENSIONS_REGISTER_EVENT,
11
10
  NEURALWATT_EXTENSIONS_REQUEST_EVENT,
12
- type NeuralwattFeatureId,
13
- registerNeuralwattSettings,
14
- } from "../../config";
15
- import { getNeuralwattApiKey } from "../../lib/env";
16
- import { fetchQuotas } from "../../lib/neuralwatt-api";
17
- import type { NeuralwattQuotas } from "../../types/quota-api";
18
- import {
19
11
  NEURALWATT_QUOTAS_REQUEST_EVENT,
20
12
  NEURALWATT_QUOTAS_UPDATED_EVENT,
13
+ type NeuralwattFeatureId,
21
14
  type NeuralwattQuotasUpdatedPayload,
22
- } from "../../types/quota-events";
15
+ } from "../../src/events";
16
+ import { fetchQuotas } from "../../src/lib/neuralwatt-api";
17
+ import type { NeuralwattQuotas } from "../../src/types/quota-api";
18
+ import { getNeuralwattApiKey } from "../_shared/auth";
19
+ import { registerNeuralwattSettings } from "./commands/settings";
23
20
  import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
24
21
  import {
25
22
  getNeuralwattModels,
@@ -38,16 +35,23 @@ import { wrapNeuralwattStreamSimple } from "./stream-simple";
38
35
 
39
36
  const HEADER_EMIT_THROTTLE_MS = 5_000;
40
37
 
38
+ function emitConfigUpdated(pi: ExtensionAPI): void {
39
+ pi.events.emit(NEURALWATT_CONFIG_UPDATED_EVENT, {
40
+ config: configLoader.getConfig(),
41
+ });
42
+ }
43
+
41
44
  function registerNeuralwattProvider(
42
45
  pi: ExtensionAPI,
43
46
  onSseQuota: (line: string) => void,
44
47
  hiddenModels: ProviderModelConfig[] = [],
45
48
  ): void {
46
- const { includeLegacyModelIds, includeHiddenModels } =
47
- configLoader.getConfig();
49
+ const { provider: providerConfig } = configLoader.getConfig();
48
50
 
49
- const publicModels = getNeuralwattModels({ includeLegacyModelIds });
50
- const resolvedHiddenModels = includeHiddenModels
51
+ const publicModels = getNeuralwattModels({
52
+ includeLegacyModelIds: providerConfig.includeLegacyModelIds,
53
+ });
54
+ const resolvedHiddenModels = providerConfig.includeHiddenModels
51
55
  ? dedupeHiddenModels(hiddenModels, publicModels)
52
56
  : [];
53
57
 
@@ -109,20 +113,23 @@ export default async function (pi: ExtensionAPI) {
109
113
  // load time. `session_start` then revalidates from the live API and writes
110
114
  // the cache back. First run with no cache still warns once.
111
115
  let hiddenModels: ProviderModelConfig[] = [];
112
- if (configLoader.getConfig().includeHiddenModels) {
116
+ if (configLoader.getConfig().provider.includeHiddenModels) {
113
117
  hiddenModels = loadCachedHiddenModels();
114
118
  }
115
119
  let hiddenModelsLoaded = false;
116
120
  let hiddenModelsAbort: AbortController | undefined;
117
121
 
122
+ let lastSseEmitAt = 0;
123
+
118
124
  const handleSseQuota = (line: string) => {
125
+ const now = Date.now();
126
+ if (now - lastSseEmitAt < HEADER_EMIT_THROTTLE_MS) return;
127
+
119
128
  const quotas = updateQuotasFromSseComment(latestQuotas, line);
120
129
  if (!quotas || quotas === latestQuotas) return;
121
- latestQuotas = quotas;
122
- pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, {
123
- quotas,
124
- source: "sse",
125
- });
130
+
131
+ lastSseEmitAt = now;
132
+ emitQuotas(quotas, "sse");
126
133
  };
127
134
 
128
135
  registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
@@ -139,7 +146,7 @@ export default async function (pi: ExtensionAPI) {
139
146
  // cache so previously discovered models are available immediately without
140
147
  // waiting for the next session_start revalidation.
141
148
  if (
142
- configLoader.getConfig().includeHiddenModels &&
149
+ configLoader.getConfig().provider.includeHiddenModels &&
143
150
  !hiddenModelsLoaded &&
144
151
  hiddenModels.length === 0
145
152
  ) {
@@ -257,15 +264,19 @@ export default async function (pi: ExtensionAPI) {
257
264
 
258
265
  pi.on("session_start", async (_event, ctx) => {
259
266
  pendingRateLimitInfo = undefined;
260
- for (const message of configLoader.drainMessages()) {
261
- ctx.ui.notify(message, "warning");
267
+ const messages = [...new Set(configLoader.drainMessages())];
268
+ if (messages.length > 0) {
269
+ ctx.ui.notify(messages.join("\n"), "info");
262
270
  }
263
271
 
264
272
  loadedFeatures.clear();
265
273
  pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
266
274
  emitConfigUpdated(pi);
267
275
 
268
- if (!hiddenModelsLoaded && configLoader.getConfig().includeHiddenModels) {
276
+ if (
277
+ !hiddenModelsLoaded &&
278
+ configLoader.getConfig().provider.includeHiddenModels
279
+ ) {
269
280
  hiddenModelsLoaded = true;
270
281
  hiddenModelsAbort?.abort();
271
282
  hiddenModelsAbort = new AbortController();
@@ -2,9 +2,9 @@ import type {
2
2
  AuthStorage,
3
3
  ProviderModelConfig,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import { getNeuralwattApiKey } from "../../../lib/env";
6
- import { fetchNeuralwattModels } from "../../../lib/neuralwatt-api";
7
- import type { NeuralwattApiModel } from "../../../types/models-api";
5
+ import { fetchNeuralwattModels } from "../../../src/lib/neuralwatt-api";
6
+ import type { NeuralwattApiModel } from "../../../src/types/models-api";
7
+ import { getNeuralwattApiKey } from "../../_shared/auth";
8
8
  import { NEURALWATT_MODELS } from "./public-models";
9
9
 
10
10
  // Per-ID overrides for known hidden models. The authenticated /v1/models endpoint
@@ -1,8 +1,8 @@
1
1
  import type { AuthStorage } from "@earendil-works/pi-coding-agent";
2
- import { getNeuralwattApiKey } from "../../lib/env";
3
- import { fetchQuotas } from "../../lib/neuralwatt-api";
4
- import type { NeuralwattQuotas } from "../../types/quota-api";
5
- import { parseQuotaHeaders } from "../../types/quota-events";
2
+ import { parseQuotaHeaders } from "../../src/events";
3
+ import { fetchQuotas } from "../../src/lib/neuralwatt-api";
4
+ import type { NeuralwattQuotas } from "../../src/types/quota-api";
5
+ import { getNeuralwattApiKey } from "../_shared/auth";
6
6
 
7
7
  export function buildQuotasFromHeaders(
8
8
  headers: Record<string, string>,
@@ -1,4 +1,4 @@
1
- import type { NeuralwattQuotas } from "../../types/quota-api";
1
+ import type { NeuralwattQuotas } from "../../src/types/quota-api";
2
2
 
3
3
  const JOULES_PER_KWH = 3_600_000;
4
4
 
@@ -2,29 +2,27 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+ import { configLoader } from "../../src/config";
5
6
  import {
6
- configLoader,
7
7
  NEURALWATT_CONFIG_UPDATED_EVENT,
8
8
  NEURALWATT_EXTENSIONS_REGISTER_EVENT,
9
9
  NEURALWATT_EXTENSIONS_REQUEST_EVENT,
10
- type NeuralwattConfigUpdatedPayload,
11
- } from "../../config";
12
- import {
13
10
  NEURALWATT_QUOTAS_UPDATED_EVENT,
11
+ type NeuralwattConfigUpdatedPayload,
14
12
  type NeuralwattQuotasUpdatedPayload,
15
- } from "../../types/quota-events";
13
+ } from "../../src/events";
16
14
  import { checkQuotas, clearAlertState } from "./notifier";
17
15
 
18
16
  export default async function (pi: ExtensionAPI) {
19
17
  await configLoader.load();
20
18
 
21
- let enabled = configLoader.getConfig().quotaWarnings;
22
- let currentProvider: string | undefined;
19
+ let enabled = configLoader.getConfig().quotaWarnings.enabled;
23
20
  let currentContext: ExtensionContext | undefined;
24
21
 
25
22
  // Listen for config changes at runtime
26
23
  pi.events.on(NEURALWATT_CONFIG_UPDATED_EVENT, (data: unknown) => {
27
- enabled = (data as NeuralwattConfigUpdatedPayload).config.quotaWarnings;
24
+ enabled = (data as NeuralwattConfigUpdatedPayload).config.quotaWarnings
25
+ .enabled;
28
26
 
29
27
  if (!enabled) {
30
28
  clearAlertState();
@@ -34,36 +32,29 @@ export default async function (pi: ExtensionAPI) {
34
32
  pi.events.on(NEURALWATT_QUOTAS_UPDATED_EVENT, (data: unknown) => {
35
33
  if (!enabled) return;
36
34
  if (!data || typeof data !== "object") return;
37
- if (currentProvider !== "neuralwatt" || !currentContext) return;
38
- const { quotas, source } = data as NeuralwattQuotasUpdatedPayload;
39
- checkQuotas(currentContext, quotas, source === "header");
35
+ if (!currentContext) return;
36
+ if (currentContext.model?.provider !== "neuralwatt") return;
37
+
38
+ const { quotas } = data as NeuralwattQuotasUpdatedPayload;
39
+ checkQuotas(currentContext, quotas);
40
40
  });
41
41
 
42
42
  pi.on("session_start", async (_event, ctx) => {
43
43
  currentContext = ctx;
44
- currentProvider = ctx.model?.provider;
45
44
  if (ctx.model?.provider !== "neuralwatt") return;
46
45
  clearAlertState();
47
46
  });
48
47
 
49
48
  pi.on("model_select", (_event, ctx) => {
50
49
  currentContext = ctx;
51
- currentProvider = ctx.model?.provider;
52
- if (ctx.model?.provider !== "neuralwatt") {
53
- clearAlertState();
54
- return;
55
- }
56
- clearAlertState();
57
50
  });
58
51
 
59
52
  pi.on("session_before_switch", (_event, ctx) => {
60
53
  currentContext = ctx;
61
- currentProvider = ctx.model?.provider;
62
54
  });
63
55
 
64
56
  pi.on("session_shutdown", () => {
65
57
  currentContext = undefined;
66
- currentProvider = undefined;
67
58
  clearAlertState();
68
59
  });
69
60
 
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { NeuralwattQuotas } from "../../types/quota-api";
3
- import { formatKwh, formatUsd } from "../../utils/quota-format";
2
+ import type { NeuralwattQuotas } from "../../src/types/quota-api";
3
+ import { formatKwh, formatUsd } from "../../src/utils/quota-format";
4
4
 
5
5
  export type WarningSeverity = "warning" | "critical";
6
6
 
@@ -11,6 +11,8 @@ interface AlertState {
11
11
  lastNotifiedAt: number;
12
12
  }
13
13
 
14
+ // Module-level state so cooldowns survive across invocations of checkQuotas()
15
+ // within the same Pi runtime.
14
16
  const alerts = new Map<string, AlertState>();
15
17
 
16
18
  export function clearAlertState(): void {
@@ -22,9 +24,9 @@ function shouldNotify(key: string, severity: WarningSeverity): boolean {
22
24
  if (!state) return true;
23
25
 
24
26
  const order: WarningSeverity[] = ["warning", "critical"];
25
- if (order.indexOf(severity) > order.indexOf(state.lastSeverity)) return true;
26
-
27
- if (severity === "critical") return true;
27
+ const currentIndex = order.indexOf(severity);
28
+ const lastIndex = order.indexOf(state.lastSeverity);
29
+ if (currentIndex > lastIndex) return true;
28
30
 
29
31
  return Date.now() - state.lastNotifiedAt >= COOLDOWN_MS;
30
32
  }
@@ -40,7 +42,6 @@ function markNotified(key: string, severity: WarningSeverity): void {
40
42
  export function checkQuotas(
41
43
  ctx: ExtensionContext,
42
44
  quotas: NeuralwattQuotas,
43
- skipAlreadyWarned: boolean,
44
45
  ): void {
45
46
  if (!ctx.hasUI) return;
46
47
 
@@ -55,7 +56,7 @@ export function checkQuotas(
55
56
  if (pct <= 25) {
56
57
  const severity: WarningSeverity = pct <= 10 ? "critical" : "warning";
57
58
  const key = "credits";
58
- if (!skipAlreadyWarned || shouldNotify(key, severity)) {
59
+ if (shouldNotify(key, severity)) {
59
60
  markNotified(key, severity);
60
61
  warnings.push(
61
62
  `Credits: ${pct.toFixed(0)}% remaining (${formatUsd(credits_remaining_usd)} of ${formatUsd(total_credits_usd)})`,
@@ -77,7 +78,7 @@ export function checkQuotas(
77
78
  ? "critical"
78
79
  : "warning";
79
80
  const key = "energy";
80
- if (!skipAlreadyWarned || shouldNotify(key, severity)) {
81
+ if (shouldNotify(key, severity)) {
81
82
  markNotified(key, severity);
82
83
  const tag = in_overage ? " [OVERAGE]" : "";
83
84
  warnings.push(