@slkiser/opencode-quota 1.1.3 → 1.3.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 +30 -10
- package/dist/lib/api-key-resolver.d.ts +83 -0
- package/dist/lib/api-key-resolver.d.ts.map +1 -0
- package/dist/lib/api-key-resolver.js +113 -0
- package/dist/lib/api-key-resolver.js.map +1 -0
- package/dist/lib/chutes-config.d.ts +8 -7
- package/dist/lib/chutes-config.d.ts.map +1 -1
- package/dist/lib/chutes-config.js +32 -128
- package/dist/lib/chutes-config.js.map +1 -1
- package/dist/lib/chutes.d.ts.map +1 -1
- package/dist/lib/chutes.js +1 -17
- package/dist/lib/chutes.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -48
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +1 -24
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/env-template.d.ts +25 -0
- package/dist/lib/env-template.d.ts.map +1 -0
- package/dist/lib/env-template.js +32 -0
- package/dist/lib/env-template.js.map +1 -0
- package/dist/lib/firmware-config.d.ts +1 -7
- package/dist/lib/firmware-config.d.ts.map +1 -1
- package/dist/lib/firmware-config.js +26 -148
- package/dist/lib/firmware-config.js.map +1 -1
- package/dist/lib/firmware.d.ts.map +1 -1
- package/dist/lib/firmware.js +1 -17
- package/dist/lib/firmware.js.map +1 -1
- package/dist/lib/format-utils.d.ts +56 -0
- package/dist/lib/format-utils.d.ts.map +1 -0
- package/dist/lib/format-utils.js +101 -0
- package/dist/lib/format-utils.js.map +1 -0
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +2 -67
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/google.d.ts.map +1 -1
- package/dist/lib/google.js +2 -24
- package/dist/lib/google.js.map +1 -1
- package/dist/lib/http.d.ts +14 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +34 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/jsonc.d.ts +25 -0
- package/dist/lib/jsonc.d.ts.map +1 -0
- package/dist/lib/jsonc.js +73 -0
- package/dist/lib/jsonc.js.map +1 -0
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +1 -17
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-storage.d.ts +27 -0
- package/dist/lib/opencode-storage.d.ts.map +1 -1
- package/dist/lib/opencode-storage.js +67 -0
- package/dist/lib/opencode-storage.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +5 -50
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-stats.d.ts +1 -0
- package/dist/lib/quota-stats.d.ts.map +1 -1
- package/dist/lib/quota-stats.js +15 -5
- package/dist/lib/quota-stats.js.map +1 -1
- package/dist/lib/quota-status.d.ts +7 -0
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +10 -0
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/toast-format-grouped.d.ts.map +1 -1
- package/dist/lib/toast-format-grouped.js +1 -66
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +359 -103
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -12,10 +12,109 @@ import { formatQuotaRows } from "./lib/format.js";
|
|
|
12
12
|
import { formatQuotaCommand } from "./lib/quota-command-format.js";
|
|
13
13
|
import { getProviders } from "./providers/registry.js";
|
|
14
14
|
import { tool } from "@opencode-ai/plugin";
|
|
15
|
-
import { aggregateUsage, getSessionTokenSummary } from "./lib/quota-stats.js";
|
|
15
|
+
import { aggregateUsage, getSessionTokenSummary, SessionNotFoundError } from "./lib/quota-stats.js";
|
|
16
16
|
import { formatQuotaStatsReport } from "./lib/quota-stats-format.js";
|
|
17
17
|
import { buildQuotaStatusReport } from "./lib/quota-status.js";
|
|
18
18
|
import { refreshGoogleTokensForAllAccounts } from "./lib/google.js";
|
|
19
|
+
/** All token report command specifications */
|
|
20
|
+
const TOKEN_REPORT_COMMANDS = [
|
|
21
|
+
{
|
|
22
|
+
id: "tokens_today",
|
|
23
|
+
legacyId: "quota_today",
|
|
24
|
+
template: "/tokens_today",
|
|
25
|
+
legacyTemplate: "/quota_today",
|
|
26
|
+
description: "Token + official API cost summary for today (calendar day, local timezone).",
|
|
27
|
+
title: "Tokens used (Today) (/tokens_today)",
|
|
28
|
+
metadataTitle: "Tokens used (Today)",
|
|
29
|
+
kind: "today",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "tokens_daily",
|
|
33
|
+
legacyId: "quota_daily",
|
|
34
|
+
template: "/tokens_daily",
|
|
35
|
+
legacyTemplate: "/quota_daily",
|
|
36
|
+
description: "Token + official API cost summary for the last 24 hours (rolling).",
|
|
37
|
+
title: "Tokens used (Last 24 Hours) (/tokens_daily)",
|
|
38
|
+
metadataTitle: "Tokens used (Last 24 Hours)",
|
|
39
|
+
kind: "rolling",
|
|
40
|
+
windowMs: 24 * 60 * 60 * 1000,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "tokens_weekly",
|
|
44
|
+
legacyId: "quota_weekly",
|
|
45
|
+
template: "/tokens_weekly",
|
|
46
|
+
legacyTemplate: "/quota_weekly",
|
|
47
|
+
description: "Token + official API cost summary for the last 7 days (rolling).",
|
|
48
|
+
title: "Tokens used (Last 7 Days) (/tokens_weekly)",
|
|
49
|
+
metadataTitle: "Tokens used (Last 7 Days)",
|
|
50
|
+
kind: "rolling",
|
|
51
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "tokens_monthly",
|
|
55
|
+
legacyId: "quota_monthly",
|
|
56
|
+
template: "/tokens_monthly",
|
|
57
|
+
legacyTemplate: "/quota_monthly",
|
|
58
|
+
description: "Token + official API cost summary for the last 30 days (rolling).",
|
|
59
|
+
title: "Tokens used (Last 30 Days) (/tokens_monthly)",
|
|
60
|
+
metadataTitle: "Tokens used (Last 30 Days)",
|
|
61
|
+
kind: "rolling",
|
|
62
|
+
windowMs: 30 * 24 * 60 * 60 * 1000,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "tokens_all",
|
|
66
|
+
legacyId: "quota_all",
|
|
67
|
+
template: "/tokens_all",
|
|
68
|
+
legacyTemplate: "/quota_all",
|
|
69
|
+
description: "Token + official API cost summary for all locally saved OpenCode history.",
|
|
70
|
+
title: "Tokens used (All Time) (/tokens_all)",
|
|
71
|
+
metadataTitle: "Tokens used (All Time)",
|
|
72
|
+
kind: "all",
|
|
73
|
+
topModels: 12,
|
|
74
|
+
topSessions: 12,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "tokens_session",
|
|
78
|
+
legacyId: "quota_session",
|
|
79
|
+
template: "/tokens_session",
|
|
80
|
+
legacyTemplate: "/quota_session",
|
|
81
|
+
description: "Token + official API cost summary for current session only.",
|
|
82
|
+
title: "Tokens used (Current Session) (/tokens_session)",
|
|
83
|
+
metadataTitle: "Tokens used (Current Session)",
|
|
84
|
+
kind: "session",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "tokens_between",
|
|
88
|
+
legacyId: "quota_between",
|
|
89
|
+
template: "/tokens_between",
|
|
90
|
+
legacyTemplate: "/quota_between",
|
|
91
|
+
description: "Token + cost report between two YYYY-MM-DD dates (local timezone, inclusive).",
|
|
92
|
+
titleForRange: (startYmd, endYmd) => {
|
|
93
|
+
const formatYmd = (ymd) => {
|
|
94
|
+
const y = String(ymd.y).padStart(4, "0");
|
|
95
|
+
const m = String(ymd.m).padStart(2, "0");
|
|
96
|
+
const d = String(ymd.d).padStart(2, "0");
|
|
97
|
+
return `${y}-${m}-${d}`;
|
|
98
|
+
};
|
|
99
|
+
return `Tokens used (${formatYmd(startYmd)} .. ${formatYmd(endYmd)}) (/tokens_between)`;
|
|
100
|
+
},
|
|
101
|
+
metadataTitle: "Tokens used (Date Range)",
|
|
102
|
+
kind: "between",
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
/** Build a lookup map from command ID (both new and legacy) to spec */
|
|
106
|
+
const TOKEN_REPORT_COMMANDS_BY_ID = (() => {
|
|
107
|
+
const map = new Map();
|
|
108
|
+
for (const spec of TOKEN_REPORT_COMMANDS) {
|
|
109
|
+
map.set(spec.id, spec);
|
|
110
|
+
map.set(spec.legacyId, spec);
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
})();
|
|
114
|
+
/** Check if a command is a token report command */
|
|
115
|
+
function isTokenReportCommand(cmd) {
|
|
116
|
+
return TOKEN_REPORT_COMMANDS_BY_ID.has(cmd);
|
|
117
|
+
}
|
|
19
118
|
// =============================================================================
|
|
20
119
|
// Plugin Implementation
|
|
21
120
|
// =============================================================================
|
|
@@ -58,6 +157,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
58
157
|
let configLoaded = false;
|
|
59
158
|
let configInFlight = null;
|
|
60
159
|
let configMeta = createLoadConfigMeta();
|
|
160
|
+
// Track last session token error for /quota_status diagnostics
|
|
161
|
+
let lastSessionTokenError;
|
|
61
162
|
async function refreshConfig() {
|
|
62
163
|
if (configInFlight)
|
|
63
164
|
return configInFlight;
|
|
@@ -92,6 +193,102 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
92
193
|
return { ok: false, error: "Failed to parse JSON arguments." };
|
|
93
194
|
}
|
|
94
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Parse a YYYY-MM-DD string. Returns null if invalid format or invalid date.
|
|
198
|
+
*/
|
|
199
|
+
function parseYyyyMmDd(input) {
|
|
200
|
+
const pattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
201
|
+
if (!pattern.test(input))
|
|
202
|
+
return null;
|
|
203
|
+
const [yStr, mStr, dStr] = input.split("-");
|
|
204
|
+
const y = parseInt(yStr, 10);
|
|
205
|
+
const m = parseInt(mStr, 10);
|
|
206
|
+
const d = parseInt(dStr, 10);
|
|
207
|
+
// Validate by round-trip: construct a Date and check components match
|
|
208
|
+
const date = new Date(y, m - 1, d);
|
|
209
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
|
|
210
|
+
return null; // Invalid date (e.g., 2026-02-31)
|
|
211
|
+
}
|
|
212
|
+
return { y, m, d };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the start of a local day (midnight) in milliseconds.
|
|
216
|
+
*/
|
|
217
|
+
function startOfLocalDayMs(ymd) {
|
|
218
|
+
return new Date(ymd.y, ymd.m - 1, ymd.d).getTime();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get the start of the next local day (midnight of the following day) in milliseconds.
|
|
222
|
+
* Used for inclusive end date: untilMs = startOfNextLocalDayMs(end) (exclusive upper bound).
|
|
223
|
+
*/
|
|
224
|
+
function startOfNextLocalDayMs(ymd) {
|
|
225
|
+
return new Date(ymd.y, ymd.m - 1, ymd.d + 1).getTime();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse /quota_between arguments. Supports:
|
|
229
|
+
* - Positional: "2026-01-01 2026-01-15"
|
|
230
|
+
* - JSON: {"starting_date":"2026-01-01","ending_date":"2026-01-15"}
|
|
231
|
+
*/
|
|
232
|
+
function parseQuotaBetweenArgs(input) {
|
|
233
|
+
const raw = input?.trim() || "";
|
|
234
|
+
if (!raw) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: "Missing arguments. Expected two dates in YYYY-MM-DD format.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
let startStr;
|
|
241
|
+
let endStr;
|
|
242
|
+
if (raw.startsWith("{")) {
|
|
243
|
+
// JSON format
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(raw);
|
|
246
|
+
startStr = String(parsed["starting_date"] ?? parsed["startingDate"] ?? "");
|
|
247
|
+
endStr = String(parsed["ending_date"] ?? parsed["endingDate"] ?? "");
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return { ok: false, error: "Failed to parse JSON arguments." };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Positional format: split on whitespace
|
|
255
|
+
const parts = raw.split(/\s+/);
|
|
256
|
+
if (parts.length !== 2) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: "Expected exactly two dates in YYYY-MM-DD format.",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
[startStr, endStr] = parts;
|
|
263
|
+
}
|
|
264
|
+
const startYmd = parseYyyyMmDd(startStr);
|
|
265
|
+
if (!startYmd) {
|
|
266
|
+
return { ok: false, error: `Invalid starting date: "${startStr}". Expected YYYY-MM-DD.` };
|
|
267
|
+
}
|
|
268
|
+
const endYmd = parseYyyyMmDd(endStr);
|
|
269
|
+
if (!endYmd) {
|
|
270
|
+
return { ok: false, error: `Invalid ending date: "${endStr}". Expected YYYY-MM-DD.` };
|
|
271
|
+
}
|
|
272
|
+
// Check end >= start
|
|
273
|
+
const startMs = startOfLocalDayMs(startYmd);
|
|
274
|
+
const endMs = startOfLocalDayMs(endYmd);
|
|
275
|
+
if (endMs < startMs) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: `Ending date (${endStr}) is before starting date (${startStr}).`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { ok: true, startYmd, endYmd };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Format a Ymd as YYYY-MM-DD string.
|
|
285
|
+
*/
|
|
286
|
+
function formatYmd(ymd) {
|
|
287
|
+
const y = String(ymd.y).padStart(4, "0");
|
|
288
|
+
const m = String(ymd.m).padStart(2, "0");
|
|
289
|
+
const d = String(ymd.d).padStart(2, "0");
|
|
290
|
+
return `${y}-${m}-${d}`;
|
|
291
|
+
}
|
|
95
292
|
// Best-effort async init (do not await)
|
|
96
293
|
void (async () => {
|
|
97
294
|
await refreshConfig();
|
|
@@ -237,9 +434,25 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
237
434
|
totalOutput: summary.totalOutput,
|
|
238
435
|
};
|
|
239
436
|
}
|
|
437
|
+
// Clear any previous error on success
|
|
438
|
+
lastSessionTokenError = undefined;
|
|
240
439
|
}
|
|
241
|
-
catch {
|
|
242
|
-
//
|
|
440
|
+
catch (err) {
|
|
441
|
+
// Capture error for /quota_status diagnostics
|
|
442
|
+
if (err instanceof SessionNotFoundError) {
|
|
443
|
+
lastSessionTokenError = {
|
|
444
|
+
sessionID: err.sessionID,
|
|
445
|
+
error: err.message,
|
|
446
|
+
checkedPath: err.checkedPath,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
lastSessionTokenError = {
|
|
451
|
+
sessionID,
|
|
452
|
+
error: err instanceof Error ? err.message : String(err),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// Toast still displays without session tokens
|
|
243
456
|
}
|
|
244
457
|
}
|
|
245
458
|
if (entries.length > 0) {
|
|
@@ -367,9 +580,25 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
367
580
|
totalOutput: summary.totalOutput,
|
|
368
581
|
};
|
|
369
582
|
}
|
|
583
|
+
// Clear any previous error on success
|
|
584
|
+
lastSessionTokenError = undefined;
|
|
370
585
|
}
|
|
371
|
-
catch {
|
|
372
|
-
//
|
|
586
|
+
catch (err) {
|
|
587
|
+
// Capture error for /quota_status diagnostics
|
|
588
|
+
if (err instanceof SessionNotFoundError) {
|
|
589
|
+
lastSessionTokenError = {
|
|
590
|
+
sessionID: err.sessionID,
|
|
591
|
+
error: err.message,
|
|
592
|
+
checkedPath: err.checkedPath,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
lastSessionTokenError = {
|
|
597
|
+
sessionID,
|
|
598
|
+
error: err instanceof Error ? err.message : String(err),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// Command still returns without session tokens
|
|
373
602
|
}
|
|
374
603
|
}
|
|
375
604
|
return formatQuotaCommand({ entries, errors, sessionTokens });
|
|
@@ -438,6 +667,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
438
667
|
failures: refresh.failures,
|
|
439
668
|
}
|
|
440
669
|
: { attempted: false },
|
|
670
|
+
sessionTokenError: lastSessionTokenError,
|
|
441
671
|
});
|
|
442
672
|
}
|
|
443
673
|
// Return hook implementations
|
|
@@ -446,38 +676,28 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
446
676
|
config: async (input) => {
|
|
447
677
|
const cfg = input;
|
|
448
678
|
cfg.command ??= {};
|
|
679
|
+
// Non-token commands (quota toast and diagnostics)
|
|
449
680
|
cfg.command["quota"] = {
|
|
450
681
|
template: "/quota",
|
|
451
682
|
description: "Show quota toast output in chat.",
|
|
452
683
|
};
|
|
453
|
-
cfg.command["quota_daily"] = {
|
|
454
|
-
template: "/quota_daily",
|
|
455
|
-
description: "Token + official API cost summary for the last 24 hours (rolling).",
|
|
456
|
-
};
|
|
457
|
-
cfg.command["quota_weekly"] = {
|
|
458
|
-
template: "/quota_weekly",
|
|
459
|
-
description: "Token + official API cost summary for the last 7 days (rolling).",
|
|
460
|
-
};
|
|
461
|
-
cfg.command["quota_monthly"] = {
|
|
462
|
-
template: "/quota_monthly",
|
|
463
|
-
description: "Token + official API cost summary for the last 30 days (rolling).",
|
|
464
|
-
};
|
|
465
|
-
cfg.command["quota_all"] = {
|
|
466
|
-
template: "/quota_all",
|
|
467
|
-
description: "Token + official API cost summary for all locally saved OpenCode history.",
|
|
468
|
-
};
|
|
469
684
|
cfg.command["quota_status"] = {
|
|
470
685
|
template: "/quota_status",
|
|
471
686
|
description: "Diagnostics for toast + pricing + local storage (includes unknown pricing report).",
|
|
472
687
|
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
688
|
+
// Register token report commands (primary /tokens_* and legacy /quota_* aliases)
|
|
689
|
+
for (const spec of TOKEN_REPORT_COMMANDS) {
|
|
690
|
+
// Primary command (/tokens_*)
|
|
691
|
+
cfg.command[spec.id] = {
|
|
692
|
+
template: spec.template,
|
|
693
|
+
description: spec.description,
|
|
694
|
+
};
|
|
695
|
+
// Legacy alias (/quota_*) for backwards compatibility
|
|
696
|
+
cfg.command[spec.legacyId] = {
|
|
697
|
+
template: spec.legacyTemplate,
|
|
698
|
+
description: `${spec.description} (Legacy alias for /${spec.id})`,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
481
701
|
},
|
|
482
702
|
"command.execute.before": async (input) => {
|
|
483
703
|
const cmd = input.command;
|
|
@@ -516,53 +736,70 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
516
736
|
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
517
737
|
}
|
|
518
738
|
const untilMs = Date.now();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
739
|
+
// Handle token report commands generically (both /tokens_* and legacy /quota_* aliases)
|
|
740
|
+
if (isTokenReportCommand(cmd)) {
|
|
741
|
+
const spec = TOKEN_REPORT_COMMANDS_BY_ID.get(cmd);
|
|
742
|
+
if (spec.kind === "between") {
|
|
743
|
+
// Special handling for date range command
|
|
744
|
+
const parsed = parseQuotaBetweenArgs(input.arguments);
|
|
745
|
+
if (!parsed.ok) {
|
|
746
|
+
await injectRawOutput(sessionID, `Invalid arguments for /${spec.id}\n\n${parsed.error}\n\nExpected: /${spec.id} YYYY-MM-DD YYYY-MM-DD\nExample: /${spec.id} 2026-01-01 2026-01-15`);
|
|
747
|
+
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
748
|
+
}
|
|
749
|
+
const sinceMs = startOfLocalDayMs(parsed.startYmd);
|
|
750
|
+
const rangeUntilMs = startOfNextLocalDayMs(parsed.endYmd); // Exclusive upper bound for inclusive end date
|
|
751
|
+
const out = await buildQuotaReport({
|
|
752
|
+
title: spec.titleForRange(parsed.startYmd, parsed.endYmd),
|
|
753
|
+
sinceMs,
|
|
754
|
+
untilMs: rangeUntilMs,
|
|
755
|
+
sessionID,
|
|
756
|
+
});
|
|
757
|
+
await injectRawOutput(sessionID, out);
|
|
758
|
+
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
759
|
+
}
|
|
760
|
+
// Non-between token report commands
|
|
761
|
+
let sinceMs;
|
|
762
|
+
let filterSessionID;
|
|
763
|
+
let sessionOnly;
|
|
764
|
+
let topModels;
|
|
765
|
+
let topSessions;
|
|
766
|
+
switch (spec.kind) {
|
|
767
|
+
case "rolling":
|
|
768
|
+
sinceMs = untilMs - spec.windowMs;
|
|
769
|
+
break;
|
|
770
|
+
case "today": {
|
|
771
|
+
const now = new Date();
|
|
772
|
+
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
773
|
+
sinceMs = startOfDay.getTime();
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
case "session":
|
|
777
|
+
filterSessionID = sessionID;
|
|
778
|
+
sessionOnly = true;
|
|
779
|
+
break;
|
|
780
|
+
case "all":
|
|
781
|
+
topModels = spec.topModels;
|
|
782
|
+
topSessions = spec.topSessions;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
543
785
|
const out = await buildQuotaReport({
|
|
544
|
-
title:
|
|
786
|
+
title: spec.title,
|
|
545
787
|
sinceMs,
|
|
546
|
-
untilMs,
|
|
788
|
+
untilMs: spec.kind === "rolling" || spec.kind === "today" ? untilMs : undefined,
|
|
547
789
|
sessionID,
|
|
790
|
+
filterSessionID,
|
|
791
|
+
sessionOnly,
|
|
792
|
+
topModels,
|
|
793
|
+
topSessions,
|
|
548
794
|
});
|
|
549
795
|
await injectRawOutput(sessionID, out);
|
|
550
796
|
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
551
797
|
}
|
|
552
|
-
|
|
553
|
-
const out = await buildQuotaReport({
|
|
554
|
-
title: "Quota (/quota_all)",
|
|
555
|
-
sessionID,
|
|
556
|
-
topModels: 12,
|
|
557
|
-
topSessions: 12,
|
|
558
|
-
});
|
|
559
|
-
await injectRawOutput(sessionID, out);
|
|
560
|
-
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
561
|
-
}
|
|
798
|
+
// Handle /quota_status (diagnostics - not a token report)
|
|
562
799
|
if (cmd === "quota_status") {
|
|
563
800
|
const parsed = parseOptionalJsonArgs(input.arguments);
|
|
564
801
|
if (!parsed.ok) {
|
|
565
|
-
await injectRawOutput(sessionID, `Invalid arguments for /quota_status\n\n${parsed.error}\n\nExample:\n/quota_status {
|
|
802
|
+
await injectRawOutput(sessionID, `Invalid arguments for /quota_status\n\n${parsed.error}\n\nExample:\n/quota_status {"refreshGoogleTokens": true}`);
|
|
566
803
|
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
567
804
|
}
|
|
568
805
|
const out = await buildStatusReport({
|
|
@@ -575,31 +812,6 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
575
812
|
await injectRawOutput(sessionID, out);
|
|
576
813
|
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
577
814
|
}
|
|
578
|
-
if (cmd === "quota_today") {
|
|
579
|
-
// Calendar day in local timezone: midnight to now
|
|
580
|
-
const now = new Date();
|
|
581
|
-
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
582
|
-
const sinceMs = startOfDay.getTime();
|
|
583
|
-
const untilMs = now.getTime();
|
|
584
|
-
const out = await buildQuotaReport({
|
|
585
|
-
title: "Quota (/quota_today)",
|
|
586
|
-
sinceMs,
|
|
587
|
-
untilMs,
|
|
588
|
-
sessionID,
|
|
589
|
-
});
|
|
590
|
-
await injectRawOutput(sessionID, out);
|
|
591
|
-
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
592
|
-
}
|
|
593
|
-
if (cmd === "quota_session") {
|
|
594
|
-
const out = await buildQuotaReport({
|
|
595
|
-
title: "Quota (/quota_session)",
|
|
596
|
-
sessionID,
|
|
597
|
-
filterSessionID: sessionID,
|
|
598
|
-
sessionOnly: true,
|
|
599
|
-
});
|
|
600
|
-
await injectRawOutput(sessionID, out);
|
|
601
|
-
throw new Error("__QUOTA_COMMAND_HANDLED__");
|
|
602
|
-
}
|
|
603
815
|
},
|
|
604
816
|
tool: {
|
|
605
817
|
quota_daily: tool({
|
|
@@ -609,12 +821,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
609
821
|
const untilMs = Date.now();
|
|
610
822
|
const sinceMs = untilMs - 24 * 60 * 60 * 1000;
|
|
611
823
|
const out = await buildQuotaReport({
|
|
612
|
-
title: "
|
|
824
|
+
title: "Tokens used (Last 24 Hours) (/tokens_daily)",
|
|
613
825
|
sinceMs,
|
|
614
826
|
untilMs,
|
|
615
827
|
sessionID: context.sessionID,
|
|
616
828
|
});
|
|
617
|
-
context.metadata({ title: "
|
|
829
|
+
context.metadata({ title: "Tokens used (Last 24 Hours)" });
|
|
618
830
|
await injectRawOutput(context.sessionID, out);
|
|
619
831
|
return ""; // Empty return - output already injected with noReply
|
|
620
832
|
},
|
|
@@ -626,12 +838,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
626
838
|
const untilMs = Date.now();
|
|
627
839
|
const sinceMs = untilMs - 7 * 24 * 60 * 60 * 1000;
|
|
628
840
|
const out = await buildQuotaReport({
|
|
629
|
-
title: "
|
|
841
|
+
title: "Tokens used (Last 7 Days) (/tokens_weekly)",
|
|
630
842
|
sinceMs,
|
|
631
843
|
untilMs,
|
|
632
844
|
sessionID: context.sessionID,
|
|
633
845
|
});
|
|
634
|
-
context.metadata({ title: "
|
|
846
|
+
context.metadata({ title: "Tokens used (Last 7 Days)" });
|
|
635
847
|
await injectRawOutput(context.sessionID, out);
|
|
636
848
|
return ""; // Empty return - output already injected with noReply
|
|
637
849
|
},
|
|
@@ -643,12 +855,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
643
855
|
const untilMs = Date.now();
|
|
644
856
|
const sinceMs = untilMs - 30 * 24 * 60 * 60 * 1000;
|
|
645
857
|
const out = await buildQuotaReport({
|
|
646
|
-
title: "
|
|
858
|
+
title: "Tokens used (Last 30 Days) (/tokens_monthly)",
|
|
647
859
|
sinceMs,
|
|
648
860
|
untilMs,
|
|
649
861
|
sessionID: context.sessionID,
|
|
650
862
|
});
|
|
651
|
-
context.metadata({ title: "
|
|
863
|
+
context.metadata({ title: "Tokens used (Last 30 Days)" });
|
|
652
864
|
await injectRawOutput(context.sessionID, out);
|
|
653
865
|
return ""; // Empty return - output already injected with noReply
|
|
654
866
|
},
|
|
@@ -658,12 +870,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
658
870
|
args: {},
|
|
659
871
|
async execute(_args, context) {
|
|
660
872
|
const out = await buildQuotaReport({
|
|
661
|
-
title: "
|
|
873
|
+
title: "Tokens used (All Time) (/tokens_all)",
|
|
662
874
|
sessionID: context.sessionID,
|
|
663
875
|
topModels: 12,
|
|
664
876
|
topSessions: 12,
|
|
665
877
|
});
|
|
666
|
-
context.metadata({ title: "
|
|
878
|
+
context.metadata({ title: "Tokens used (All Time)" });
|
|
667
879
|
await injectRawOutput(context.sessionID, out);
|
|
668
880
|
return ""; // Empty return - output already injected with noReply
|
|
669
881
|
},
|
|
@@ -706,12 +918,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
706
918
|
const sinceMs = startOfDay.getTime();
|
|
707
919
|
const untilMs = now.getTime();
|
|
708
920
|
const out = await buildQuotaReport({
|
|
709
|
-
title: "
|
|
921
|
+
title: "Tokens used (Today) (/tokens_today)",
|
|
710
922
|
sinceMs,
|
|
711
923
|
untilMs,
|
|
712
924
|
sessionID: context.sessionID,
|
|
713
925
|
});
|
|
714
|
-
context.metadata({ title: "
|
|
926
|
+
context.metadata({ title: "Tokens used (Today)" });
|
|
715
927
|
await injectRawOutput(context.sessionID, out);
|
|
716
928
|
return ""; // Empty return - output already injected with noReply
|
|
717
929
|
},
|
|
@@ -721,12 +933,56 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
721
933
|
args: {},
|
|
722
934
|
async execute(_args, context) {
|
|
723
935
|
const out = await buildQuotaReport({
|
|
724
|
-
title: "
|
|
936
|
+
title: "Tokens used (Current Session) (/tokens_session)",
|
|
725
937
|
sessionID: context.sessionID,
|
|
726
938
|
filterSessionID: context.sessionID,
|
|
727
939
|
sessionOnly: true,
|
|
728
940
|
});
|
|
729
|
-
context.metadata({ title: "
|
|
941
|
+
context.metadata({ title: "Tokens used (Current Session)" });
|
|
942
|
+
await injectRawOutput(context.sessionID, out);
|
|
943
|
+
return ""; // Empty return - output already injected with noReply
|
|
944
|
+
},
|
|
945
|
+
}),
|
|
946
|
+
quota_between: tool({
|
|
947
|
+
description: "Token + official API cost summary between two YYYY-MM-DD dates (local timezone, inclusive).",
|
|
948
|
+
args: {
|
|
949
|
+
startingDate: tool.schema
|
|
950
|
+
.string()
|
|
951
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
952
|
+
.describe("Starting date in YYYY-MM-DD format (local timezone)"),
|
|
953
|
+
endingDate: tool.schema
|
|
954
|
+
.string()
|
|
955
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
956
|
+
.describe("Ending date in YYYY-MM-DD format (local timezone, inclusive)"),
|
|
957
|
+
},
|
|
958
|
+
async execute(args, context) {
|
|
959
|
+
const startYmd = parseYyyyMmDd(args.startingDate);
|
|
960
|
+
if (!startYmd) {
|
|
961
|
+
await injectRawOutput(context.sessionID, `Invalid starting date: "${args.startingDate}". Expected YYYY-MM-DD.`);
|
|
962
|
+
return "";
|
|
963
|
+
}
|
|
964
|
+
const endYmd = parseYyyyMmDd(args.endingDate);
|
|
965
|
+
if (!endYmd) {
|
|
966
|
+
await injectRawOutput(context.sessionID, `Invalid ending date: "${args.endingDate}". Expected YYYY-MM-DD.`);
|
|
967
|
+
return "";
|
|
968
|
+
}
|
|
969
|
+
const startMs = startOfLocalDayMs(startYmd);
|
|
970
|
+
const endMs = startOfLocalDayMs(endYmd);
|
|
971
|
+
if (endMs < startMs) {
|
|
972
|
+
await injectRawOutput(context.sessionID, `Ending date (${args.endingDate}) is before starting date (${args.startingDate}).`);
|
|
973
|
+
return "";
|
|
974
|
+
}
|
|
975
|
+
const sinceMs = startMs;
|
|
976
|
+
const untilMs = startOfNextLocalDayMs(endYmd); // Exclusive upper bound for inclusive end date
|
|
977
|
+
const startStr = formatYmd(startYmd);
|
|
978
|
+
const endStr = formatYmd(endYmd);
|
|
979
|
+
const out = await buildQuotaReport({
|
|
980
|
+
title: `Tokens used (${startStr} .. ${endStr}) (/tokens_between)`,
|
|
981
|
+
sinceMs,
|
|
982
|
+
untilMs,
|
|
983
|
+
sessionID: context.sessionID,
|
|
984
|
+
});
|
|
985
|
+
context.metadata({ title: "Tokens used (Date Range)" });
|
|
730
986
|
await injectRawOutput(context.sessionID, out);
|
|
731
987
|
return ""; // Empty return - output already injected with noReply
|
|
732
988
|
},
|