@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.
Files changed (72) hide show
  1. package/README.md +30 -10
  2. package/dist/lib/api-key-resolver.d.ts +83 -0
  3. package/dist/lib/api-key-resolver.d.ts.map +1 -0
  4. package/dist/lib/api-key-resolver.js +113 -0
  5. package/dist/lib/api-key-resolver.js.map +1 -0
  6. package/dist/lib/chutes-config.d.ts +8 -7
  7. package/dist/lib/chutes-config.d.ts.map +1 -1
  8. package/dist/lib/chutes-config.js +32 -128
  9. package/dist/lib/chutes-config.js.map +1 -1
  10. package/dist/lib/chutes.d.ts.map +1 -1
  11. package/dist/lib/chutes.js +1 -17
  12. package/dist/lib/chutes.js.map +1 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +1 -48
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/copilot.d.ts.map +1 -1
  17. package/dist/lib/copilot.js +1 -24
  18. package/dist/lib/copilot.js.map +1 -1
  19. package/dist/lib/env-template.d.ts +25 -0
  20. package/dist/lib/env-template.d.ts.map +1 -0
  21. package/dist/lib/env-template.js +32 -0
  22. package/dist/lib/env-template.js.map +1 -0
  23. package/dist/lib/firmware-config.d.ts +1 -7
  24. package/dist/lib/firmware-config.d.ts.map +1 -1
  25. package/dist/lib/firmware-config.js +26 -148
  26. package/dist/lib/firmware-config.js.map +1 -1
  27. package/dist/lib/firmware.d.ts.map +1 -1
  28. package/dist/lib/firmware.js +1 -17
  29. package/dist/lib/firmware.js.map +1 -1
  30. package/dist/lib/format-utils.d.ts +56 -0
  31. package/dist/lib/format-utils.d.ts.map +1 -0
  32. package/dist/lib/format-utils.js +101 -0
  33. package/dist/lib/format-utils.js.map +1 -0
  34. package/dist/lib/format.d.ts.map +1 -1
  35. package/dist/lib/format.js +2 -67
  36. package/dist/lib/format.js.map +1 -1
  37. package/dist/lib/google.d.ts.map +1 -1
  38. package/dist/lib/google.js +2 -24
  39. package/dist/lib/google.js.map +1 -1
  40. package/dist/lib/http.d.ts +14 -0
  41. package/dist/lib/http.d.ts.map +1 -0
  42. package/dist/lib/http.js +34 -0
  43. package/dist/lib/http.js.map +1 -0
  44. package/dist/lib/jsonc.d.ts +25 -0
  45. package/dist/lib/jsonc.d.ts.map +1 -0
  46. package/dist/lib/jsonc.js +73 -0
  47. package/dist/lib/jsonc.js.map +1 -0
  48. package/dist/lib/openai.d.ts.map +1 -1
  49. package/dist/lib/openai.js +1 -17
  50. package/dist/lib/openai.js.map +1 -1
  51. package/dist/lib/opencode-storage.d.ts +27 -0
  52. package/dist/lib/opencode-storage.d.ts.map +1 -1
  53. package/dist/lib/opencode-storage.js +67 -0
  54. package/dist/lib/opencode-storage.js.map +1 -1
  55. package/dist/lib/quota-command-format.d.ts.map +1 -1
  56. package/dist/lib/quota-command-format.js +5 -50
  57. package/dist/lib/quota-command-format.js.map +1 -1
  58. package/dist/lib/quota-stats.d.ts +1 -0
  59. package/dist/lib/quota-stats.d.ts.map +1 -1
  60. package/dist/lib/quota-stats.js +15 -5
  61. package/dist/lib/quota-stats.js.map +1 -1
  62. package/dist/lib/quota-status.d.ts +7 -0
  63. package/dist/lib/quota-status.d.ts.map +1 -1
  64. package/dist/lib/quota-status.js +10 -0
  65. package/dist/lib/quota-status.js.map +1 -1
  66. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  67. package/dist/lib/toast-format-grouped.js +1 -66
  68. package/dist/lib/toast-format-grouped.js.map +1 -1
  69. package/dist/plugin.d.ts.map +1 -1
  70. package/dist/plugin.js +359 -103
  71. package/dist/plugin.js.map +1 -1
  72. 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
- // Ignore errors fetching session tokens - it's a nice-to-have
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
- // Ignore errors fetching session tokens - it's a nice-to-have
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
- cfg.command["quota_today"] = {
474
- template: "/quota_today",
475
- description: "Token + official API cost summary for today (calendar day, local timezone).",
476
- };
477
- cfg.command["quota_session"] = {
478
- template: "/quota_session",
479
- description: "Token + official API cost summary for current session only.",
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
- if (cmd === "quota_daily") {
520
- const sinceMs = untilMs - 24 * 60 * 60 * 1000;
521
- const out = await buildQuotaReport({
522
- title: "Quota (/quota_daily)",
523
- sinceMs,
524
- untilMs,
525
- sessionID,
526
- });
527
- await injectRawOutput(sessionID, out);
528
- throw new Error("__QUOTA_COMMAND_HANDLED__");
529
- }
530
- if (cmd === "quota_weekly") {
531
- const sinceMs = untilMs - 7 * 24 * 60 * 60 * 1000;
532
- const out = await buildQuotaReport({
533
- title: "Quota (/quota_weekly)",
534
- sinceMs,
535
- untilMs,
536
- sessionID,
537
- });
538
- await injectRawOutput(sessionID, out);
539
- throw new Error("__QUOTA_COMMAND_HANDLED__");
540
- }
541
- if (cmd === "quota_monthly") {
542
- const sinceMs = untilMs - 30 * 24 * 60 * 60 * 1000;
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: "Quota (/quota_monthly)",
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
- if (cmd === "quota_all") {
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 {\"refreshGoogleTokens\": true}`);
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: "Quota (/quota_daily)",
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: "Quota Daily" });
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: "Quota (/quota_weekly)",
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: "Quota Weekly" });
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: "Quota (/quota_monthly)",
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: "Quota Monthly" });
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: "Quota (/quota_all)",
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: "Quota All" });
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: "Quota (/quota_today)",
921
+ title: "Tokens used (Today) (/tokens_today)",
710
922
  sinceMs,
711
923
  untilMs,
712
924
  sessionID: context.sessionID,
713
925
  });
714
- context.metadata({ title: "Quota Today" });
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: "Quota (/quota_session)",
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: "Quota Session" });
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
  },