@ereinha/opencode-enhanced-quotas 1.0.3 → 1.0.5

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/dist/defaults.js CHANGED
@@ -43,9 +43,15 @@ export const DEFAULT_CONFIG = {
43
43
  strategy: "most_critical",
44
44
  },
45
45
  {
46
- id: "codex-smart",
47
- name: "Codex Usage",
48
- sources: ["codex-primary", "codex-secondary"],
46
+ id: "codex-session",
47
+ name: "Codex Session",
48
+ sources: ["codex-session"],
49
+ strategy: "most_critical",
50
+ },
51
+ {
52
+ id: "codex-window",
53
+ name: "Codex Window",
54
+ sources: ["codex-window"],
49
55
  strategy: "most_critical",
50
56
  },
51
57
  ],
package/dist/index.js CHANGED
@@ -128,152 +128,183 @@ export const QuotaHubPlugin = async ({ client, $, directory, serverUrl, }) => {
128
128
  lineCount: lines.length,
129
129
  };
130
130
  };
131
+ // ─── Helper: send result to chat without triggering AI ─────────────
132
+ const sendCommandResult = async (sessionID, text) => {
133
+ await client.session.prompt({
134
+ path: { id: sessionID },
135
+ body: {
136
+ noReply: true,
137
+ parts: [{
138
+ type: "text",
139
+ text,
140
+ ignored: true,
141
+ }],
142
+ },
143
+ });
144
+ };
131
145
  // ─── Hooks ────────────────────────────────────────────────────────────
132
146
  const hooks = {
133
147
  /**
134
- * Intercept /usage and /mymodels commands to execute them deterministically
135
- * without any AI involvement. Reads from the quota cache directly.
136
- *
137
- * By setting output.parts, we replace whatever would normally be sent to the
138
- * AI with our own pre-computed text. The AI never sees these commands.
148
+ * Register /usage and /mymodels commands in OpenCode's UI with descriptions.
149
+ * This makes them appear in the command palette (ctrl+p) and autocomplete.
150
+ */
151
+ config: async (opencodeConfig) => {
152
+ opencodeConfig.command ??= {};
153
+ opencodeConfig.command["usage"] = {
154
+ template: "",
155
+ description: "Display quota usage for all configured providers",
156
+ };
157
+ opencodeConfig.command["mymodels"] = {
158
+ template: "",
159
+ description: "Show model availability filtered by quota status",
160
+ };
161
+ },
162
+ /**
163
+ * Intercept /usage and /mymodels commands to execute them deterministically.
164
+ * No AI involvement. Reads from the quota cache directly, sends the result
165
+ * via client.session.prompt with noReply:true, then throws to abort the
166
+ * normal command flow (same pattern as DCP plugin).
139
167
  */
140
- "command.execute.before": async (input, output) => {
168
+ "command.execute.before": async (input, _output) => {
141
169
  const command = input.command;
142
- const args = input.arguments;
170
+ const args = input.arguments || "";
143
171
  // Only handle our custom commands
144
172
  if (command !== "usage" && command !== "mymodels") {
145
173
  return;
146
174
  }
147
175
  await ensureInit().catch(() => { });
148
176
  const snapshot = quotaCache?.getSnapshot();
149
- const quotas = snapshot?.data || [];
177
+ const rawQuotas = snapshot?.data || [];
178
+ // Run through the aggregation/processing pipeline so we get
179
+ // the same groups shown in the footer (Session, Window, Flash, etc.)
180
+ const processed = quotaService.processQuotas(rawQuotas);
181
+ // Filter out unlimited / unmetered quotas (limit: null)
182
+ const quotas = processed.filter((q) => q.limit !== null && q.limit > 0);
150
183
  // ── /usage ────────────────────────────────────────────────
151
184
  if (command === "usage") {
185
+ let result;
152
186
  if (quotas.length === 0) {
153
- output.parts = [{
154
- type: "text",
155
- text: "No quota data available. Quota providers may not be configured or initialized yet.",
156
- }];
157
- return;
158
- }
159
- // Parse arguments: format=table|json|compact provider=<name>
160
- const argParts = args.split(" ").filter(Boolean);
161
- const formatArg = argParts.find((a) => a.startsWith("format="));
162
- const providerArg = argParts.find((a) => a.startsWith("provider="));
163
- const format = formatArg?.split("=")[1] || "table";
164
- const providerFilter = providerArg?.split("=")[1];
165
- let filtered = quotas;
166
- if (providerFilter) {
167
- filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
168
- }
169
- let result = "";
170
- if (format === "json") {
171
- result = JSON.stringify(filtered.map((q) => ({
172
- id: q.id,
173
- provider: q.providerName,
174
- used: q.used,
175
- limit: q.limit,
176
- percentUsed: q.limit ? Math.round((q.used / q.limit) * 100) : 0,
177
- reset: q.reset || q.predictedReset || null,
178
- unit: q.unit,
179
- })), null, 2);
180
- }
181
- else if (format === "compact") {
182
- result = filtered
183
- .map((q) => {
184
- const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
185
- return `${q.providerName}: ${q.used}/${q.limit || "unlimited"} (${pct}%)`;
186
- })
187
- .join(" | ");
187
+ result = "No quota data available. Quota providers may not be configured or initialized yet.";
188
188
  }
189
189
  else {
190
- // Default: table format using the existing renderQuotaTable
191
- const config = quotaService.getConfig();
192
- const lines = renderQuotaTable(filtered, {
193
- progressBarConfig: config.progressBar,
194
- tableConfig: config.table,
195
- }).map((l) => l.line);
196
- const showMode = config.progressBar?.show ?? "used";
197
- const modeLabel = showMode === "available" ? "(Remaining)" : "(Used)";
198
- const header = `_Opencode Quotas ${modeLabel}_`;
199
- const updatedAt = snapshot?.fetchedAt
200
- ? `Last updated: ${snapshot.fetchedAt.toLocaleTimeString()}`
201
- : "";
202
- result = [header, ...lines, updatedAt].filter(Boolean).join("\n");
190
+ // Parse arguments: format=table|json|compact provider=<name>
191
+ const argParts = args.split(" ").filter(Boolean);
192
+ const formatArg = argParts.find((a) => a.startsWith("format="));
193
+ const providerArg = argParts.find((a) => a.startsWith("provider="));
194
+ const format = formatArg?.split("=")[1] || "table";
195
+ const providerFilter = providerArg?.split("=")[1];
196
+ let filtered = quotas;
197
+ if (providerFilter) {
198
+ filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
199
+ }
200
+ if (format === "json") {
201
+ result = JSON.stringify(filtered.map((q) => ({
202
+ id: q.id,
203
+ provider: q.providerName,
204
+ used: q.used,
205
+ limit: q.limit,
206
+ percentUsed: q.limit ? Math.round((q.used / q.limit) * 100) : 0,
207
+ reset: q.reset || q.predictedReset || null,
208
+ unit: q.unit,
209
+ })), null, 2);
210
+ }
211
+ else if (format === "compact") {
212
+ result = filtered
213
+ .map((q) => {
214
+ const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
215
+ return `${q.providerName}: ${q.used}/${q.limit || "unlimited"} (${pct}%)`;
216
+ })
217
+ .join(" | ");
218
+ }
219
+ else {
220
+ // Default: table format using the existing renderQuotaTable
221
+ const config = quotaService.getConfig();
222
+ const lines = renderQuotaTable(filtered, {
223
+ progressBarConfig: config.progressBar,
224
+ tableConfig: config.table,
225
+ }).map((l) => l.line);
226
+ const showMode = config.progressBar?.show ?? "used";
227
+ const modeLabel = showMode === "available" ? "(Remaining)" : "(Used)";
228
+ const header = `_Opencode Quotas ${modeLabel}_`;
229
+ const updatedAt = snapshot?.fetchedAt
230
+ ? `Last updated: ${snapshot.fetchedAt.toLocaleTimeString()}`
231
+ : "";
232
+ result = [header, ...lines, updatedAt].filter(Boolean).join("\n");
233
+ }
203
234
  }
204
- output.parts = [{ type: "text", text: result }];
205
- return;
235
+ await sendCommandResult(input.sessionID, result);
236
+ throw new Error("__QUOTAHUB_USAGE_HANDLED__");
206
237
  }
207
238
  // ── /mymodels ─────────────────────────────────────────────
208
239
  if (command === "mymodels") {
240
+ let result;
209
241
  if (quotas.length === 0) {
210
- output.parts = [{
211
- type: "text",
212
- text: "No quota data available to determine model availability.",
213
- }];
214
- return;
215
- }
216
- // Parse arguments: status=all|available|limited|exhausted provider=<name>
217
- const argParts = args.split(" ").filter(Boolean);
218
- const statusArg = argParts.find((a) => a.startsWith("status="));
219
- const providerArg = argParts.find((a) => a.startsWith("provider="));
220
- const statusFilter = statusArg?.split("=")[1] || "all";
221
- const providerFilter = providerArg?.split("=")[1];
222
- let filtered = quotas;
223
- if (providerFilter) {
224
- filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
225
- }
226
- const available = [];
227
- const limited = [];
228
- const exhausted = [];
229
- for (const q of filtered) {
230
- const pct = q.limit ? (q.used / q.limit) * 100 : 0;
231
- if (pct >= 100)
232
- exhausted.push(q);
233
- else if (pct >= 80)
234
- limited.push(q);
235
- else
236
- available.push(q);
242
+ result = "No quota data available to determine model availability.";
237
243
  }
238
- const lines = ["Model Availability by Quota Status:"];
239
- if (statusFilter === "all" || statusFilter === "available") {
240
- if (available.length > 0) {
241
- lines.push("\n[OK] Available:");
242
- for (const q of available) {
243
- const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
244
- const reset = q.reset ? ` (resets: ${q.reset})` : "";
245
- lines.push(` + ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
244
+ else {
245
+ // Parse arguments: status=all|available|limited|exhausted provider=<name>
246
+ const argParts = args.split(" ").filter(Boolean);
247
+ const statusArg = argParts.find((a) => a.startsWith("status="));
248
+ const providerArg = argParts.find((a) => a.startsWith("provider="));
249
+ const statusFilter = statusArg?.split("=")[1] || "all";
250
+ const providerFilter = providerArg?.split("=")[1];
251
+ let filtered = quotas;
252
+ if (providerFilter) {
253
+ filtered = quotas.filter((q) => q.providerName.toLowerCase().includes(providerFilter.toLowerCase()));
254
+ }
255
+ const available = [];
256
+ const limited = [];
257
+ const exhausted = [];
258
+ for (const q of filtered) {
259
+ const pct = q.limit ? (q.used / q.limit) * 100 : 0;
260
+ if (pct >= 100)
261
+ exhausted.push(q);
262
+ else if (pct >= 80)
263
+ limited.push(q);
264
+ else
265
+ available.push(q);
266
+ }
267
+ const lines = ["Model Availability by Quota Status:"];
268
+ if (statusFilter === "all" || statusFilter === "available") {
269
+ if (available.length > 0) {
270
+ lines.push("\n[OK] Available:");
271
+ for (const q of available) {
272
+ const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
273
+ const reset = q.reset ? ` (resets: ${q.reset})` : "";
274
+ lines.push(` + ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
275
+ }
246
276
  }
247
277
  }
248
- }
249
- if (statusFilter === "all" || statusFilter === "limited") {
250
- if (limited.length > 0) {
251
- lines.push("\n[~] Limited (approaching limit):");
252
- for (const q of limited) {
253
- const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
254
- const reset = q.reset ? ` (resets: ${q.reset})` : "";
255
- lines.push(` ~ ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
278
+ if (statusFilter === "all" || statusFilter === "limited") {
279
+ if (limited.length > 0) {
280
+ lines.push("\n[~] Limited (approaching limit):");
281
+ for (const q of limited) {
282
+ const pct = q.limit ? Math.round((q.used / q.limit) * 100) : 0;
283
+ const reset = q.reset ? ` (resets: ${q.reset})` : "";
284
+ lines.push(` ~ ${q.providerName} - ${q.id}: ${pct}% used${reset}`);
285
+ }
256
286
  }
257
287
  }
258
- }
259
- if (statusFilter === "all" || statusFilter === "exhausted") {
260
- if (exhausted.length > 0) {
261
- lines.push("\n[!] Exhausted (unavailable):");
262
- for (const q of exhausted) {
263
- const reset = q.reset ? ` (resets: ${q.reset})` : "";
264
- lines.push(` x ${q.providerName} - ${q.id}: 100%+ used${reset}`);
288
+ if (statusFilter === "all" || statusFilter === "exhausted") {
289
+ if (exhausted.length > 0) {
290
+ lines.push("\n[!] Exhausted (unavailable):");
291
+ for (const q of exhausted) {
292
+ const reset = q.reset ? ` (resets: ${q.reset})` : "";
293
+ lines.push(` x ${q.providerName} - ${q.id}: 100%+ used${reset}`);
294
+ }
265
295
  }
266
296
  }
297
+ if (lines.length === 1) {
298
+ lines.push(`\nNo models found with status: ${statusFilter}`);
299
+ }
300
+ else {
301
+ lines.push(`\n${"-".repeat(40)}`);
302
+ lines.push(`Summary: ${available.length} available, ${limited.length} limited, ${exhausted.length} exhausted`);
303
+ }
304
+ result = lines.join("\n");
267
305
  }
268
- if (lines.length === 1) {
269
- lines.push(`\nNo models found with status: ${statusFilter}`);
270
- }
271
- else {
272
- lines.push(`\n${"-".repeat(40)}`);
273
- lines.push(`Summary: ${available.length} available, ${limited.length} limited, ${exhausted.length} exhausted`);
274
- }
275
- output.parts = [{ type: "text", text: lines.join("\n") }];
276
- return;
306
+ await sendCommandResult(input.sessionID, result);
307
+ throw new Error("__QUOTAHUB_MYMODELS_HANDLED__");
277
308
  }
278
309
  },
279
310
  /**
@@ -182,12 +182,12 @@ export function extractCodexQuota(payload) {
182
182
  ? rateLimit.secondary_window
183
183
  : null;
184
184
  if (primary) {
185
- const entry = parseRateLimitWindow("primary", "Primary", primary);
185
+ const entry = parseRateLimitWindow("session", "Session", primary);
186
186
  if (entry)
187
187
  entries.push(entry);
188
188
  }
189
189
  if (secondary) {
190
- const entry = parseRateLimitWindow("secondary", "Secondary", secondary);
190
+ const entry = parseRateLimitWindow("window", "Window", secondary);
191
191
  if (entry)
192
192
  entries.push(entry);
193
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ereinha/opencode-enhanced-quotas",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {