@aliou/pi-synthetic 0.11.0 → 0.13.0

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
@@ -73,6 +73,28 @@ Check your API usage:
73
73
  /synthetic:quotas
74
74
  ```
75
75
 
76
+ ### Usage Status
77
+
78
+ When a Synthetic model is active, the footer status bar shows live quota usage (e.g. `week:82% (↺in 3d) 5h:95%`). Colors follow the same severity assessment as quota warnings: green by default, yellow/red only when projected usage is at risk. The status auto-refreshes every 60 seconds and after each turn.
79
+
80
+ ### Quota Warnings
81
+
82
+ The extension automatically notifies you when you approach or exceed your Synthetic API quotas. Notifications fire on severity transitions only (no repeated alerts for the same level) and use correct terminology (regen/tick/resets) with precise time formatting.
83
+
84
+ - Escalation always notifies
85
+ - `high` and `critical` levels have no cooldown
86
+ - `warning` level has a 60-minute cooldown
87
+
88
+ ## Disabling Features
89
+
90
+ Each feature (provider, web search, quotas command, usage status, quota warnings) is a separate Pi extension. You can disable individual features using `pi config`:
91
+
92
+ ```
93
+ pi config extensions.disabled add @aliou/pi-synthetic/quota-warnings
94
+ ```
95
+
96
+ This prevents the quota-warnings extension from loading while keeping the rest of pi-synthetic active. Replace `quota-warnings` with `web-search`, `command-quotas`, or `provider` to disable other features.
97
+
76
98
  ## Adding or Updating Models
77
99
 
78
100
  Models are hardcoded in `src/providers/models.ts`. To add or update models:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -17,7 +17,10 @@
17
17
  "extensions": [
18
18
  "./src/extensions/provider/index.ts",
19
19
  "./src/extensions/web-search/index.ts",
20
- "./src/extensions/command-quotas/index.ts"
20
+ "./src/extensions/command-quotas/index.ts",
21
+ "./src/extensions/sub-bar-integration/index.ts",
22
+ "./src/extensions/quota-warnings/index.ts",
23
+ "./src/extensions/usage-status/index.ts"
21
24
  ],
22
25
  "video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
23
26
  },
@@ -33,6 +36,7 @@
33
36
  "@mariozechner/pi-tui": "0.61.0"
34
37
  },
35
38
  "dependencies": {
39
+ "@aliou/pi-utils-settings": "^0.13.0",
36
40
  "@aliou/pi-utils-ui": "^0.1.2"
37
41
  },
38
42
  "devDependencies": {
package/src/config.ts ADDED
@@ -0,0 +1,277 @@
1
+ import {
2
+ ConfigLoader,
3
+ type Migration,
4
+ registerSettingsCommand,
5
+ type SettingsSection,
6
+ } from "@aliou/pi-utils-settings";
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import type { SettingItem } from "@mariozechner/pi-tui";
9
+ import pkg from "../package.json" with { type: "json" };
10
+
11
+ export type SyntheticFeatureId =
12
+ | "webSearch"
13
+ | "quotasCommand"
14
+ | "subBarIntegration"
15
+ | "usageStatus"
16
+ | "quotaWarnings";
17
+
18
+ export const SYNTHETIC_EXTENSIONS_REQUEST_EVENT =
19
+ "synthetic:extensions:request" as const;
20
+
21
+ export const SYNTHETIC_EXTENSIONS_REGISTER_EVENT =
22
+ "synthetic:extensions:register" as const;
23
+
24
+ export interface SyntheticExtensionsRegisterPayload {
25
+ feature: SyntheticFeatureId;
26
+ }
27
+
28
+ /**
29
+ * Config schema version. Stamped on disk when the initial migration runs or
30
+ * when the config is seeded. Uses the package version; bumping the package
31
+ * does not retrigger migrations (we only run them when `configVersion` is
32
+ * missing), but it records which release first created the file.
33
+ */
34
+ export const SYNTHETIC_CONFIG_VERSION: string = pkg.version;
35
+
36
+ export interface SyntheticConfig {
37
+ configVersion?: string;
38
+ webSearch?: boolean;
39
+ quotasCommand?: boolean;
40
+ usageStatus?: boolean;
41
+ quotaWarnings?: boolean;
42
+ subBarIntegration?: boolean;
43
+ }
44
+
45
+ export interface ResolvedSyntheticConfig {
46
+ configVersion: string;
47
+ webSearch: boolean;
48
+ quotasCommand: boolean;
49
+ usageStatus: boolean;
50
+ quotaWarnings: boolean;
51
+ subBarIntegration: boolean;
52
+ }
53
+
54
+ const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
55
+ configVersion: SYNTHETIC_CONFIG_VERSION,
56
+ webSearch: true,
57
+ quotasCommand: true,
58
+ usageStatus: false,
59
+ quotaWarnings: false,
60
+ subBarIntegration: true,
61
+ };
62
+
63
+ // Module-level flag set when the v1 migration runs or when the global config
64
+ // is seeded for the first time. Consumed once by the provider extension to
65
+ // display a one-time notice about the new settings UI.
66
+ let pendingMigrationNotice = false;
67
+
68
+ export function hasPendingMigrationNotice(): boolean {
69
+ return pendingMigrationNotice;
70
+ }
71
+
72
+ export function clearPendingMigrationNotice(): void {
73
+ pendingMigrationNotice = false;
74
+ }
75
+
76
+ function markMigrationNoticePending(): void {
77
+ pendingMigrationNotice = true;
78
+ }
79
+
80
+ const migrations: Migration<SyntheticConfig>[] = [
81
+ {
82
+ name: "seed-defaults",
83
+ shouldRun: (config) => config.configVersion === undefined,
84
+ run: (config) => {
85
+ markMigrationNoticePending();
86
+ return {
87
+ configVersion: SYNTHETIC_CONFIG_VERSION,
88
+ webSearch: config.webSearch ?? DEFAULT_CONFIG.webSearch,
89
+ quotasCommand: config.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
90
+ usageStatus: config.usageStatus ?? DEFAULT_CONFIG.usageStatus,
91
+ quotaWarnings: config.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
92
+ subBarIntegration:
93
+ config.subBarIntegration ?? DEFAULT_CONFIG.subBarIntegration,
94
+ };
95
+ },
96
+ },
97
+ ];
98
+
99
+ const QUOTA_WARNING_THRESHOLDS_DESCRIPTION =
100
+ "Toggle warnings when your quotas reach thresholds. Thresholds: warning at 80% projected usage, high at 90%, critical at 100% for fixed windows; dynamic windows use adaptive projected thresholds based on window progress.";
101
+
102
+ export const configLoader = new ConfigLoader<
103
+ SyntheticConfig,
104
+ ResolvedSyntheticConfig
105
+ >("synthetic", DEFAULT_CONFIG, { migrations });
106
+
107
+ /**
108
+ * Seed the global config file on first use. When no config file exists in
109
+ * any scope, this writes the current defaults (with configVersion) to the
110
+ * global scope and flags the migration notice as pending.
111
+ *
112
+ * Must be called after `configLoader.load()`.
113
+ */
114
+ export async function seedSyntheticConfigIfMissing(): Promise<void> {
115
+ if (configLoader.hasConfig("global") || configLoader.hasConfig("local")) {
116
+ return;
117
+ }
118
+ markMigrationNoticePending();
119
+ try {
120
+ await configLoader.save("global", {
121
+ configVersion: SYNTHETIC_CONFIG_VERSION,
122
+ webSearch: DEFAULT_CONFIG.webSearch,
123
+ quotasCommand: DEFAULT_CONFIG.quotasCommand,
124
+ usageStatus: DEFAULT_CONFIG.usageStatus,
125
+ quotaWarnings: DEFAULT_CONFIG.quotaWarnings,
126
+ subBarIntegration: DEFAULT_CONFIG.subBarIntegration,
127
+ });
128
+ } catch {
129
+ // If the write fails, keep the notice pending so the user still sees it.
130
+ }
131
+ }
132
+
133
+ export const SYNTHETIC_CONFIG_UPDATED_EVENT =
134
+ "synthetic:config:updated" as const;
135
+
136
+ export interface SyntheticConfigUpdatedPayload {
137
+ config: ResolvedSyntheticConfig;
138
+ }
139
+
140
+ export function emitSyntheticConfigUpdated(pi: ExtensionAPI): void {
141
+ pi.events.emit(SYNTHETIC_CONFIG_UPDATED_EVENT, {
142
+ config: configLoader.getConfig(),
143
+ });
144
+ }
145
+
146
+ export interface RegisterSyntheticSettingsOptions {
147
+ getLoadedFeatures: () => Set<SyntheticFeatureId>;
148
+ }
149
+
150
+ function featureRow(
151
+ id: SyntheticFeatureId,
152
+ label: string,
153
+ description: string,
154
+ configValue: boolean,
155
+ isLoaded: boolean,
156
+ ): SettingItem {
157
+ if (isLoaded) {
158
+ return {
159
+ id,
160
+ label,
161
+ description,
162
+ currentValue: configValue ? "enabled" : "disabled",
163
+ values: ["enabled", "disabled"],
164
+ };
165
+ }
166
+ return {
167
+ id,
168
+ label,
169
+ description: `${description} (Not loaded by Pi)`,
170
+ currentValue: "unavailable",
171
+ values: [],
172
+ };
173
+ }
174
+
175
+ export function registerSyntheticSettings(
176
+ pi: ExtensionAPI,
177
+ options: RegisterSyntheticSettingsOptions,
178
+ ): void {
179
+ const { getLoadedFeatures } = options;
180
+
181
+ registerSettingsCommand<SyntheticConfig, ResolvedSyntheticConfig>(pi, {
182
+ commandName: "synthetic:settings",
183
+ commandDescription: "Configure Synthetic extension settings",
184
+ title: "Synthetic Settings",
185
+ configStore: configLoader,
186
+ buildSections: (tabConfig, resolved): SettingsSection[] => {
187
+ const loaded = getLoadedFeatures();
188
+ const webSearch = tabConfig?.webSearch ?? resolved.webSearch;
189
+ const quotasCommand = tabConfig?.quotasCommand ?? resolved.quotasCommand;
190
+ const usageStatus = tabConfig?.usageStatus ?? resolved.usageStatus;
191
+ const quotaWarnings = tabConfig?.quotaWarnings ?? resolved.quotaWarnings;
192
+ const subBarIntegration =
193
+ tabConfig?.subBarIntegration ?? resolved.subBarIntegration;
194
+
195
+ const sections: SettingsSection[] = [];
196
+
197
+ sections.push(
198
+ {
199
+ label: "Tools",
200
+ items: [
201
+ featureRow(
202
+ "webSearch",
203
+ "Web Search",
204
+ "Toggle `synthetic_web_search`, a tool for searching online with zero data retention",
205
+ webSearch,
206
+ loaded.has("webSearch"),
207
+ ),
208
+ ],
209
+ },
210
+ {
211
+ label: "Quotas",
212
+ items: [
213
+ featureRow(
214
+ "quotasCommand",
215
+ "Quotas Command",
216
+ "Toggle the `/synthetic:quotas` command, showing your quotas at a glance",
217
+ quotasCommand,
218
+ loaded.has("quotasCommand"),
219
+ ),
220
+ featureRow(
221
+ "usageStatus",
222
+ "Usage widget",
223
+ "Toggle the usage widget, showing your usage at a glance",
224
+ usageStatus,
225
+ loaded.has("usageStatus"),
226
+ ),
227
+ featureRow(
228
+ "quotaWarnings",
229
+ "Quota Warnings",
230
+ QUOTA_WARNING_THRESHOLDS_DESCRIPTION,
231
+ quotaWarnings,
232
+ loaded.has("quotaWarnings"),
233
+ ),
234
+ ],
235
+ },
236
+ {
237
+ label: "Integration",
238
+ items: [
239
+ featureRow(
240
+ "subBarIntegration",
241
+ "pi-sub-bar integration",
242
+ "Integration with `@marckrenn/pi-sub-bar`",
243
+ subBarIntegration,
244
+ loaded.has("subBarIntegration"),
245
+ ),
246
+ ],
247
+ },
248
+ );
249
+
250
+ return sections;
251
+ },
252
+ onSettingChange: (id, newValue, config) => {
253
+ if (!getLoadedFeatures().has(id as SyntheticFeatureId)) {
254
+ return null;
255
+ }
256
+
257
+ const enabled = newValue === "enabled";
258
+ switch (id) {
259
+ case "webSearch":
260
+ return { ...config, webSearch: enabled };
261
+ case "quotasCommand":
262
+ return { ...config, quotasCommand: enabled };
263
+ case "usageStatus":
264
+ return { ...config, usageStatus: enabled };
265
+ case "quotaWarnings":
266
+ return { ...config, quotaWarnings: enabled };
267
+ case "subBarIntegration":
268
+ return { ...config, subBarIntegration: enabled };
269
+ default:
270
+ return null;
271
+ }
272
+ },
273
+ onSave: async () => {
274
+ emitSyntheticConfigUpdated(pi);
275
+ },
276
+ });
277
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { configLoader } from "../../config";
2
3
  import { getSyntheticApiKey } from "../../lib/env";
3
4
  import { fetchQuotas } from "../../utils/quotas";
4
5
  import { QuotasComponent } from "./components/quotas-display";
@@ -10,6 +11,14 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
10
11
  pi.registerCommand("synthetic:quotas", {
11
12
  description: "Display Synthetic API usage quotas",
12
13
  handler: async (_args, ctx) => {
14
+ if (!configLoader.getConfig().quotasCommand) {
15
+ ctx.ui.notify(
16
+ "Synthetic quotas command is disabled. Restart Pi to unload the command after re-enabling or disabling it.",
17
+ "warning",
18
+ );
19
+ return;
20
+ }
21
+
13
22
  const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
14
23
  if (!apiKey) {
15
24
  ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
@@ -3,167 +3,19 @@ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
3
  import type { Component, TUI } from "@mariozechner/pi-tui";
4
4
  import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
5
5
  import type { QuotasResponse } from "../../../types/quotas";
6
+ import {
7
+ assessWindow,
8
+ formatTimeRemaining,
9
+ getSeverityColor,
10
+ type QuotaWindow,
11
+ toWindows,
12
+ } from "../../../utils/quotas-severity";
6
13
 
7
14
  type QuotasState =
8
15
  | { type: "loading" }
9
16
  | { type: "error"; message: string }
10
17
  | { type: "loaded"; quotas: QuotasResponse };
11
18
 
12
- interface QuotaWindow {
13
- label: string;
14
- usedPercent: number;
15
- resetsAt: Date;
16
- windowSeconds: number;
17
- usedValue: number;
18
- limitValue: number;
19
- isCurrency?: boolean;
20
- showPace?: boolean;
21
- paceScale?: number;
22
- limited?: boolean;
23
- nextAmount?: string;
24
- nextLabel?: string;
25
- }
26
-
27
- /** Safely compute percentage, guarding against division by zero */
28
- function safePercent(used: number, limit: number): number {
29
- if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return 0;
30
- return Math.max(0, Math.min(100, (used / limit) * 100));
31
- }
32
-
33
- /** Parse currency string like "$1,234.56" to number */
34
- function parseCurrency(value: string): number {
35
- const n = Number(value.replace(/[^0-9.-]/g, ""));
36
- return Number.isFinite(n) ? n : 0;
37
- }
38
-
39
- function toWindows(quotas: QuotasResponse): QuotaWindow[] {
40
- const windows: QuotaWindow[] = [];
41
-
42
- if (quotas.weeklyTokenLimit) {
43
- const { weeklyTokenLimit } = quotas;
44
- const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
45
- const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
46
- windows.push({
47
- label: "Credits / week",
48
- usedPercent: Math.max(
49
- 0,
50
- Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
51
- ),
52
- resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
53
- windowSeconds: 24 * 60 * 60,
54
- usedValue: limitValue - remainingValue,
55
- limitValue,
56
- isCurrency: true,
57
- showPace: true,
58
- paceScale: 1 / 7,
59
- nextAmount: `+${weeklyTokenLimit.nextRegenCredits}`,
60
- nextLabel: "Next regen",
61
- });
62
- }
63
-
64
- if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
65
- const { rollingFiveHourLimit } = quotas;
66
- const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
67
- const tickAmount =
68
- rollingFiveHourLimit.tickPercent * rollingFiveHourLimit.max;
69
- windows.push({
70
- label: "Requests / 5h",
71
- usedPercent: safePercent(used, rollingFiveHourLimit.max),
72
- resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
73
- windowSeconds: 5 * 60 * 60,
74
- usedValue: Math.round(used),
75
- limitValue: rollingFiveHourLimit.max,
76
- showPace: false,
77
- limited: rollingFiveHourLimit.limited,
78
- nextAmount: `+${tickAmount.toFixed(1)}`,
79
- nextLabel: "Next tick",
80
- });
81
- }
82
-
83
- if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
84
- const { hourly } = quotas.search;
85
- windows.push({
86
- label: "Search / hour",
87
- usedPercent: safePercent(hourly.requests, hourly.limit),
88
- resetsAt: new Date(hourly.renewsAt),
89
- windowSeconds: 60 * 60,
90
- usedValue: hourly.requests,
91
- limitValue: hourly.limit,
92
- showPace: true,
93
- paceScale: 1,
94
- nextLabel: "Resets",
95
- });
96
- }
97
-
98
- if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
99
- windows.push({
100
- label: "Free Tool Calls / day",
101
- usedPercent: safePercent(
102
- quotas.freeToolCalls.requests,
103
- quotas.freeToolCalls.limit,
104
- ),
105
- resetsAt: new Date(quotas.freeToolCalls.renewsAt),
106
- windowSeconds: 24 * 60 * 60,
107
- usedValue: quotas.freeToolCalls.requests,
108
- limitValue: quotas.freeToolCalls.limit,
109
- showPace: true,
110
- paceScale: 1,
111
- nextLabel: "Resets",
112
- });
113
- }
114
-
115
- return windows;
116
- }
117
-
118
- function getPacePercent(window: QuotaWindow): number | null {
119
- const totalMs = window.windowSeconds * 1000;
120
- if (totalMs <= 0) return null;
121
- const remainingMs = window.resetsAt.getTime() - Date.now();
122
- const elapsedMs = totalMs - remainingMs;
123
- return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
124
- }
125
-
126
- function getProjectedPercent(
127
- usedPercent: number,
128
- pacePercent: number | null,
129
- ): number {
130
- if (pacePercent === null) return usedPercent;
131
- const effectivePace = Math.max(5, pacePercent);
132
- return Math.max(0, (usedPercent / effectivePace) * 100);
133
- }
134
-
135
- function getSeverity(
136
- projectedPercent: number,
137
- pacePercent: number | null,
138
- ): "success" | "warning" | "error" {
139
- if (pacePercent === null) {
140
- if (projectedPercent >= 100) return "error";
141
- if (projectedPercent >= 90) return "warning";
142
- return "success";
143
- }
144
- // Dynamic thresholds based on window progress
145
- const progress = pacePercent / 100;
146
- const warnThreshold = 260 - (260 - 120) * progress;
147
- const highThreshold = 320 - (320 - 145) * progress;
148
- const criticalThreshold = 400 - (400 - 170) * progress;
149
-
150
- if (projectedPercent >= criticalThreshold) return "error";
151
- if (projectedPercent >= highThreshold) return "error";
152
- if (projectedPercent >= warnThreshold) return "warning";
153
- return "success";
154
- }
155
-
156
- function formatTimeRemaining(date: Date): string {
157
- const ms = date.getTime() - Date.now();
158
- if (ms <= 0) return "now";
159
- const totalMins = Math.ceil(ms / (1000 * 60));
160
- const hours = Math.floor(totalMins / 60);
161
- const mins = totalMins % 60;
162
- if (hours >= 1) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
163
- const totalSecs = Math.ceil(ms / 1000);
164
- return totalMins >= 1 ? `${totalMins}m` : `${totalSecs}s`;
165
- }
166
-
167
19
  /**
168
20
  * Convert a foreground ANSI escape to its background equivalent.
169
21
  * Handles truecolor (38;2), 256-color (38;5), and basic (3X) escapes.
@@ -298,7 +150,6 @@ export class QuotasComponent implements Component {
298
150
  width,
299
151
  ),
300
152
  );
301
- lines.push("");
302
153
 
303
154
  switch (this.state.type) {
304
155
  case "loading":
@@ -334,6 +185,8 @@ export class QuotasComponent implements Component {
334
185
  const windows = toWindows(quotas);
335
186
  const barWidth = Math.min(50, Math.max(20, contentWidth - 20));
336
187
 
188
+ lines.push("");
189
+
337
190
  for (const window of windows) {
338
191
  lines.push(...this.renderWindow(window, barWidth, maxWidth));
339
192
  lines.push("");
@@ -355,15 +208,8 @@ export class QuotasComponent implements Component {
355
208
  const lines: string[] = [];
356
209
  const theme = this.theme;
357
210
 
358
- const rawPace = window.showPace ? getPacePercent(window) : null;
359
- const pacePercent =
360
- rawPace !== null ? rawPace * (window.paceScale ?? 1) : null;
361
- const projectedPercent = getProjectedPercent(
362
- window.usedPercent,
363
- pacePercent,
364
- );
365
- let severity = getSeverity(projectedPercent, pacePercent);
366
- if (window.limited) severity = "error";
211
+ const assessment = assessWindow(window);
212
+ const color = getSeverityColor(assessment.severity);
367
213
 
368
214
  // Label
369
215
  lines.push(
@@ -375,8 +221,8 @@ export class QuotasComponent implements Component {
375
221
  window.usedPercent,
376
222
  barWidth,
377
223
  theme,
378
- severity,
379
- pacePercent,
224
+ color,
225
+ assessment.pacePercent,
380
226
  );
381
227
  const usedStr = window.isCurrency
382
228
  ? `${Math.round(window.usedPercent)}%/$${window.limitValue.toFixed(2)}`
@@ -384,7 +230,7 @@ export class QuotasComponent implements Component {
384
230
  const limitedBadge = window.limited ? theme.fg("error", " LIMITED") : "";
385
231
  lines.push(
386
232
  truncateToWidth(
387
- ` ${bar} ${theme.fg(severity, usedStr)}${limitedBadge}`,
233
+ ` ${bar} ${theme.fg(color, usedStr)}${limitedBadge}`,
388
234
  maxWidth,
389
235
  ),
390
236
  );
@@ -1,8 +1,23 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ configLoader,
4
+ SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
5
+ SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
6
+ } from "../../config";
2
7
  import { registerQuotasCommand } from "./command";
3
- import { registerSubIntegration } from "./sub-integration";
4
8
 
5
9
  export default async function (pi: ExtensionAPI) {
6
- registerQuotasCommand(pi);
7
- registerSubIntegration(pi);
10
+ await configLoader.load();
11
+
12
+ const config = configLoader.getConfig();
13
+
14
+ if (config.quotasCommand) {
15
+ registerQuotasCommand(pi);
16
+ }
17
+
18
+ pi.events.on(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, () => {
19
+ pi.events.emit(SYNTHETIC_EXTENSIONS_REGISTER_EVENT, {
20
+ feature: "quotasCommand",
21
+ });
22
+ });
8
23
  }