opencode-quotas 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +344 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/cli.d.ts +3 -0
  7. package/dist/src/cli.d.ts.map +1 -0
  8. package/dist/src/cli.js +42 -0
  9. package/dist/src/cli.js.map +1 -0
  10. package/dist/src/constants.d.ts +9 -0
  11. package/dist/src/constants.d.ts.map +1 -0
  12. package/dist/src/constants.js +15 -0
  13. package/dist/src/constants.js.map +1 -0
  14. package/dist/src/defaults.d.ts +3 -0
  15. package/dist/src/defaults.d.ts.map +1 -0
  16. package/dist/src/defaults.js +52 -0
  17. package/dist/src/defaults.js.map +1 -0
  18. package/dist/src/index.d.ts +6 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +265 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/interfaces.d.ts +197 -0
  23. package/dist/src/interfaces.d.ts.map +1 -0
  24. package/dist/src/interfaces.js +2 -0
  25. package/dist/src/interfaces.js.map +1 -0
  26. package/dist/src/logger.d.ts +14 -0
  27. package/dist/src/logger.d.ts.map +1 -0
  28. package/dist/src/logger.js +44 -0
  29. package/dist/src/logger.js.map +1 -0
  30. package/dist/src/plugin-state.d.ts +20 -0
  31. package/dist/src/plugin-state.d.ts.map +1 -0
  32. package/dist/src/plugin-state.js +53 -0
  33. package/dist/src/plugin-state.js.map +1 -0
  34. package/dist/src/providers/antigravity/auth.d.ts +12 -0
  35. package/dist/src/providers/antigravity/auth.d.ts.map +1 -0
  36. package/dist/src/providers/antigravity/auth.js +109 -0
  37. package/dist/src/providers/antigravity/auth.js.map +1 -0
  38. package/dist/src/providers/antigravity/index.d.ts +2 -0
  39. package/dist/src/providers/antigravity/index.d.ts.map +1 -0
  40. package/dist/src/providers/antigravity/index.js +2 -0
  41. package/dist/src/providers/antigravity/index.js.map +1 -0
  42. package/dist/src/providers/antigravity/provider.d.ts +34 -0
  43. package/dist/src/providers/antigravity/provider.d.ts.map +1 -0
  44. package/dist/src/providers/antigravity/provider.js +216 -0
  45. package/dist/src/providers/antigravity/provider.js.map +1 -0
  46. package/dist/src/providers/codex.d.ts +4 -0
  47. package/dist/src/providers/codex.d.ts.map +1 -0
  48. package/dist/src/providers/codex.js +242 -0
  49. package/dist/src/providers/codex.js.map +1 -0
  50. package/dist/src/providers/github.d.ts +4 -0
  51. package/dist/src/providers/github.d.ts.map +1 -0
  52. package/dist/src/providers/github.js +139 -0
  53. package/dist/src/providers/github.js.map +1 -0
  54. package/dist/src/quota-cache.d.ts +26 -0
  55. package/dist/src/quota-cache.d.ts.map +1 -0
  56. package/dist/src/quota-cache.js +107 -0
  57. package/dist/src/quota-cache.js.map +1 -0
  58. package/dist/src/registry.d.ts +3 -0
  59. package/dist/src/registry.d.ts.map +1 -0
  60. package/dist/src/registry.js +23 -0
  61. package/dist/src/registry.js.map +1 -0
  62. package/dist/src/services/aggregation-service.d.ts +34 -0
  63. package/dist/src/services/aggregation-service.d.ts.map +1 -0
  64. package/dist/src/services/aggregation-service.js +89 -0
  65. package/dist/src/services/aggregation-service.js.map +1 -0
  66. package/dist/src/services/config-loader.d.ts +25 -0
  67. package/dist/src/services/config-loader.d.ts.map +1 -0
  68. package/dist/src/services/config-loader.js +105 -0
  69. package/dist/src/services/config-loader.js.map +1 -0
  70. package/dist/src/services/history-service.d.ts +15 -0
  71. package/dist/src/services/history-service.d.ts.map +1 -0
  72. package/dist/src/services/history-service.js +99 -0
  73. package/dist/src/services/history-service.js.map +1 -0
  74. package/dist/src/services/prediction-engine.d.ts +47 -0
  75. package/dist/src/services/prediction-engine.d.ts.map +1 -0
  76. package/dist/src/services/prediction-engine.js +94 -0
  77. package/dist/src/services/prediction-engine.js.map +1 -0
  78. package/dist/src/services/quota-service.d.ts +41 -0
  79. package/dist/src/services/quota-service.d.ts.map +1 -0
  80. package/dist/src/services/quota-service.js +257 -0
  81. package/dist/src/services/quota-service.js.map +1 -0
  82. package/dist/src/tools/quotas.d.ts +11 -0
  83. package/dist/src/tools/quotas.d.ts.map +1 -0
  84. package/dist/src/tools/quotas.js +62 -0
  85. package/dist/src/tools/quotas.js.map +1 -0
  86. package/dist/src/ui/progress-bar.d.ts +20 -0
  87. package/dist/src/ui/progress-bar.d.ts.map +1 -0
  88. package/dist/src/ui/progress-bar.js +150 -0
  89. package/dist/src/ui/progress-bar.js.map +1 -0
  90. package/dist/src/ui/quota-table.d.ts +15 -0
  91. package/dist/src/ui/quota-table.d.ts.map +1 -0
  92. package/dist/src/ui/quota-table.js +136 -0
  93. package/dist/src/ui/quota-table.js.map +1 -0
  94. package/dist/src/utils/debug.d.ts +1 -0
  95. package/dist/src/utils/debug.d.ts.map +1 -0
  96. package/dist/src/utils/debug.js +3 -0
  97. package/dist/src/utils/debug.js.map +1 -0
  98. package/dist/src/utils/paths.d.ts +7 -0
  99. package/dist/src/utils/paths.d.ts.map +1 -0
  100. package/dist/src/utils/paths.js +37 -0
  101. package/dist/src/utils/paths.js.map +1 -0
  102. package/dist/src/utils/time.d.ts +3 -0
  103. package/dist/src/utils/time.d.ts.map +1 -0
  104. package/dist/src/utils/time.js +38 -0
  105. package/dist/src/utils/time.js.map +1 -0
  106. package/dist/src/utils/validation.d.ts +6 -0
  107. package/dist/src/utils/validation.d.ts.map +1 -0
  108. package/dist/src/utils/validation.js +66 -0
  109. package/dist/src/utils/validation.js.map +1 -0
  110. package/package.json +42 -0
  111. package/src/cli.ts +53 -0
  112. package/src/constants.ts +17 -0
  113. package/src/defaults.ts +53 -0
  114. package/src/index.ts +338 -0
  115. package/src/interfaces.ts +258 -0
  116. package/src/logger.ts +55 -0
  117. package/src/plugin-state.ts +65 -0
  118. package/src/providers/antigravity/auth.ts +163 -0
  119. package/src/providers/antigravity/index.ts +1 -0
  120. package/src/providers/antigravity/provider.ts +337 -0
  121. package/src/providers/codex.ts +327 -0
  122. package/src/providers/github.ts +161 -0
  123. package/src/quota-cache.ts +157 -0
  124. package/src/registry.ts +30 -0
  125. package/src/services/aggregation-service.ts +116 -0
  126. package/src/services/config-loader.ts +124 -0
  127. package/src/services/history-service.ts +116 -0
  128. package/src/services/prediction-engine.ts +133 -0
  129. package/src/services/quota-service.ts +343 -0
  130. package/src/tools/quotas.ts +78 -0
  131. package/src/ui/progress-bar.ts +204 -0
  132. package/src/ui/quota-table.ts +173 -0
  133. package/src/utils/debug.ts +1 -0
  134. package/src/utils/paths.ts +41 -0
  135. package/src/utils/time.ts +40 -0
  136. package/src/utils/validation.ts +63 -0
@@ -0,0 +1,204 @@
1
+ import { type AnsiColor, type ProgressBarConfig, type GradientLevel } from "../interfaces";
2
+ import { isValidNumber, clamp } from "../utils/validation";
3
+
4
+ const DEFAULT_BAR_WIDTH = 20;
5
+ const DEFAULT_FILLED_CHAR = "█";
6
+ const DEFAULT_EMPTY_CHAR = "░";
7
+
8
+ const DEFAULT_GRADIENTS: GradientLevel[] = [
9
+ { threshold: 0.5, color: "green" },
10
+ { threshold: 0.8, color: "yellow" },
11
+ { threshold: 1.0, color: "red" },
12
+ ];
13
+
14
+ const ANSI_CODES: Record<AnsiColor, string> = {
15
+ red: "\x1b[31m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ blue: "\x1b[34m",
19
+ magenta: "\x1b[35m",
20
+ cyan: "\x1b[36m",
21
+ white: "\x1b[37m",
22
+ gray: "\x1b[90m",
23
+ bold: "\x1b[1m",
24
+ dim: "\x1b[2m",
25
+ reset: "\x1b[0m",
26
+ };
27
+
28
+ function shouldUseColor(config?: ProgressBarConfig): boolean {
29
+ // FORCE_COLOR should take precedence when explicitly set
30
+ if (process.env.FORCE_COLOR !== undefined) return true;
31
+
32
+ // Respect explicit no-color flags
33
+ if (process.env.NO_COLOR !== undefined) return false;
34
+ if (process.env.OPENCODE_QUOTAS_NO_COLOR !== undefined) return false;
35
+
36
+ // Respect explicit config request (useful for tests/environments)
37
+ if (config?.color === true) return true;
38
+
39
+ // If not a TTY, generally disable color
40
+ if (!process.stdout.isTTY) return false;
41
+
42
+ return false;
43
+ }
44
+
45
+ export function colorize(text: string, color: AnsiColor | undefined, useColor: boolean): string {
46
+ if (!useColor) return text;
47
+ if (!color || color === "reset") return text;
48
+ return `${ANSI_CODES[color]}${text}${ANSI_CODES.reset}`;
49
+ }
50
+
51
+ function formatNumber(value: number): string {
52
+ if (!Number.isFinite(value)) return "0";
53
+ if (Number.isInteger(value)) {
54
+ return `${value}`;
55
+ }
56
+ if (Math.abs(value) >= 100) {
57
+ return value.toFixed(1);
58
+ }
59
+ return value.toFixed(1);
60
+ }
61
+
62
+ export type RenderQuotaBarParts = {
63
+ labelPart: string;
64
+ bar: string;
65
+ percent: string;
66
+ valuePart: string;
67
+ detailsPart: string;
68
+ statusEmoji: string;
69
+ statusText: string;
70
+ };
71
+
72
+ export function getQuotaStatusEmoji(
73
+ ratio: number,
74
+ config: ProgressBarConfig = {}
75
+ ): string {
76
+ // Default thresholds if not provided
77
+ const gradients = config.gradients || DEFAULT_GRADIENTS;
78
+
79
+ const sorted = [...gradients].sort((a, b) => a.threshold - b.threshold);
80
+
81
+ // Find the matching level
82
+ const match = sorted.find((g) => ratio <= g.threshold);
83
+ const color = match ? match.color : sorted[sorted.length - 1]?.color || "red";
84
+
85
+ switch (color) {
86
+ case "green": return "🟢";
87
+ case "yellow": return "🟡";
88
+ case "red": return "🔴";
89
+ default: return "⚪"; // Grey/Unknown
90
+ }
91
+ }
92
+
93
+ export function getQuotaStatusText(
94
+ ratio: number,
95
+ config: ProgressBarConfig = {}
96
+ ): string {
97
+ // Default thresholds if not provided
98
+ const gradients = config.gradients || DEFAULT_GRADIENTS;
99
+
100
+ const sorted = [...gradients].sort((a, b) => a.threshold - b.threshold);
101
+
102
+ // Find the matching level
103
+ const match = sorted.find((g) => ratio <= g.threshold);
104
+ const color = match ? match.color : sorted[sorted.length - 1]?.color || "red";
105
+
106
+ switch (color) {
107
+ case "green": return "OK "; // Space for alignment
108
+ case "yellow": return "WRN";
109
+ case "red": return "ERR";
110
+ default: return "UNK";
111
+ }
112
+ }
113
+
114
+ export function renderQuotaBarParts(
115
+ used: number,
116
+ limit: number,
117
+ options: {
118
+ label: string;
119
+ unit: string;
120
+ details?: string;
121
+ config?: ProgressBarConfig;
122
+ },
123
+ ): RenderQuotaBarParts {
124
+ const config = options.config || {};
125
+ const width = config.width ?? DEFAULT_BAR_WIDTH;
126
+ const filledChar = config.filledChar ?? DEFAULT_FILLED_CHAR;
127
+ const emptyChar = config.emptyChar ?? DEFAULT_EMPTY_CHAR;
128
+ const showMode = config.show ?? "used";
129
+ const useColor = shouldUseColor(config);
130
+
131
+ // Defensive guards: normalize inputs
132
+ const usedVal = isValidNumber(used) ? Math.max(0, used) : 0;
133
+ const limitVal = isValidNumber(limit) && limit > 0 ? limit : 0;
134
+
135
+ // Calculate value and ratio based on mode
136
+ let displayValue = usedVal;
137
+ let ratio = 0;
138
+
139
+ if (limitVal > 0) {
140
+ if (showMode === "available") {
141
+ displayValue = Math.max(0, limitVal - usedVal);
142
+ }
143
+ ratio = displayValue / limitVal;
144
+ }
145
+
146
+ // Cap visual ratio at 1.0 for the bar filling, but keep actual ratio for color calculation if needed
147
+ const visualRatio = Math.min(Math.max(ratio, 0), 1);
148
+
149
+ const filledLen = Math.round(width * visualRatio);
150
+ const emptyLen = Math.max(0, width - filledLen);
151
+
152
+ // Determine Color
153
+ let barColor: AnsiColor = "reset";
154
+ let statusColor: AnsiColor = "reset";
155
+
156
+ if (config.gradients && config.gradients.length > 0) {
157
+ // Sort gradients by threshold to ensure correct evaluation
158
+ const sorted = [...config.gradients].sort(
159
+ (a, b) => a.threshold - b.threshold,
160
+ );
161
+
162
+ // Find the matching level
163
+ const match = sorted.find((g) => ratio <= g.threshold);
164
+
165
+ if (match) {
166
+ barColor = match.color;
167
+ statusColor = match.color;
168
+ } else {
169
+ // If ratio exceeds all thresholds (e.g. > 100%), use the last defined color (highest severity)
170
+ barColor = sorted[sorted.length - 1].color;
171
+ statusColor = sorted[sorted.length - 1].color;
172
+ }
173
+ }
174
+
175
+ const filledStr = filledChar.repeat(filledLen);
176
+ const emptyStr = emptyChar.repeat(emptyLen);
177
+
178
+ const bar = `${colorize(filledStr, barColor, useColor)}${emptyStr}`; // Only color filled part? Or empty too? usually just filled.
179
+
180
+ const percentRaw = limitVal > 0 ? `${Math.round(ratio * 100)}%` : "n/a";
181
+ const percentText = percentRaw === "n/a" ? percentRaw : percentRaw.padStart(4);
182
+ const percent = colorize(percentText, barColor, useColor);
183
+
184
+ const valueText = `${formatNumber(displayValue)}/${formatNumber(limitVal)} ${options.unit}`;
185
+
186
+ // Only append colon if label is present and not empty
187
+ const labelPart = options.label ? `${options.label}: ` : "";
188
+
189
+ // Determine Emoji
190
+ const statusEmoji = getQuotaStatusEmoji(ratio, config);
191
+ const statusTextRaw = getQuotaStatusText(ratio, config);
192
+ const statusText = colorize(statusTextRaw, statusColor, useColor);
193
+
194
+ return {
195
+ labelPart,
196
+ bar,
197
+ percent,
198
+ valuePart: `(${valueText})`,
199
+ detailsPart: options.details ? ` | ${options.details}` : "",
200
+ statusEmoji,
201
+ statusText,
202
+ };
203
+ }
204
+
@@ -0,0 +1,173 @@
1
+ import { renderQuotaBarParts, type RenderQuotaBarParts, colorize } from "./progress-bar";
2
+ import { type ProgressBarConfig, type QuotaData, type QuotaColumn } from "../interfaces";
3
+ import { validateQuotaData } from "../utils/validation";
4
+
5
+ type RenderedQuotaLine = {
6
+ id: string;
7
+ providerName: string;
8
+ line: string;
9
+ };
10
+
11
+ const DEFAULT_COLUMNS: QuotaColumn[] = ["status", "name", "percent", "bar", "reset", "ettl"];
12
+ const HEADERS: Record<QuotaColumn, string> = {
13
+ name: "QUOTA NAME",
14
+ bar: "UTILIZATION",
15
+ percent: "USED",
16
+ value: "VALUE",
17
+ reset: "RESET",
18
+ ettl: "ETTL",
19
+ window: "WINDOW",
20
+ info: "INFO",
21
+ status: "ST"
22
+ };
23
+
24
+ export function renderQuotaTable(
25
+ quotas: QuotaData[],
26
+ options: {
27
+ progressBarConfig?: ProgressBarConfig;
28
+ tableConfig?: { columns?: QuotaColumn[], header?: boolean };
29
+ },
30
+ ): RenderedQuotaLine[] {
31
+ if (quotas.length === 0) return [];
32
+
33
+ const columns = options.tableConfig?.columns || DEFAULT_COLUMNS;
34
+ const useColor = options.progressBarConfig?.color ?? false;
35
+
36
+ // 1. Pre-calculate cell data for every row
37
+ const rows = quotas.map((quota) => {
38
+ const validated = validateQuotaData(quota) || quota;
39
+ const isUnlimited = validated.limit === null || validated.limit <= 0;
40
+
41
+ // Render bar parts if limited
42
+ let barParts: RenderQuotaBarParts | null = null;
43
+ if (!isUnlimited) {
44
+ barParts = renderQuotaBarParts(validated.used, validated.limit!, {
45
+ label: "",
46
+ unit: validated.unit,
47
+ config: options.progressBarConfig,
48
+ });
49
+ }
50
+
51
+ const name = colorize(validated.providerName, "cyan", useColor);
52
+ const status = barParts
53
+ ? barParts.statusText
54
+ : (validated.info === "unlimited" ? colorize("OK ", "green", useColor) : colorize("UNK", "gray", useColor));
55
+
56
+ // Strip "resets in " or "resets at " prefix for cleaner table display
57
+ const resetRaw = validated.reset?.replace(/^resets (in|at) /, "") || "";
58
+ const reset = colorize(resetRaw, "gray", useColor);
59
+
60
+ // Remove leading 'in ' if present and strip '(predicted)'
61
+ const ettlRaw = validated.predictedReset?.replace(/^in\s+/i, "").replace(/\(predicted\)/, "").trim() || "-";
62
+ const ettl = colorize(ettlRaw, "gray", useColor);
63
+
64
+ return {
65
+ quota: validated,
66
+ barParts,
67
+ cells: {
68
+ name,
69
+ bar: barParts ? barParts.bar : (isUnlimited ? colorize("Unlimited", "green", useColor) : ""),
70
+ percent: barParts ? barParts.percent : "",
71
+ value: barParts ? barParts.valuePart : `${validated.used} ${validated.unit}`,
72
+ reset,
73
+ ettl,
74
+ window: validated.window || "",
75
+ info: validated.info || "",
76
+ status,
77
+ } as Record<QuotaColumn, string>
78
+ };
79
+ });
80
+
81
+ // 2. Measure widths
82
+ const widths: Record<QuotaColumn, number> = {
83
+ name: 0, bar: 0, percent: 0, value: 0, reset: 0, window: 0, info: 0, status: 0, ettl: 0
84
+ };
85
+
86
+ // Calculate max widths including headers
87
+ for (const col of columns) {
88
+ widths[col] = Math.max(widths[col], HEADERS[col].length);
89
+ }
90
+
91
+ for (const row of rows) {
92
+ for (const col of columns) {
93
+ const content = row.cells[col];
94
+ const visibleLength = content.replace(/\x1b\[[0-9;]*m/g, "").length;
95
+ if (visibleLength > widths[col]) {
96
+ widths[col] = visibleLength;
97
+ }
98
+ }
99
+ }
100
+
101
+ const outputRows: RenderedQuotaLine[] = [];
102
+
103
+ // 3. Render Header (optional)
104
+ if (options.tableConfig?.header !== false) {
105
+ const headerSegments: string[] = [];
106
+ const separatorSegments: string[] = [];
107
+
108
+ for (const col of columns) {
109
+ const width = widths[col];
110
+ const headerTitle = HEADERS[col];
111
+ // Alignments: same as content
112
+ // percent: right
113
+ // others: left
114
+
115
+ const coloredHeader = colorize(headerTitle, "bold", useColor);
116
+ let segment = "";
117
+ let sep = "";
118
+
119
+ // When padding, we need to consider visible length of the colored string vs stripped
120
+ // But here `headerTitle` is uncolored when calculating padding, so standard pad works
121
+ // provided we apply color AFTER padding, OR pad properly.
122
+ // Actually, padStart/padEnd on the uncolored string, THEN colorize.
123
+
124
+ let paddedTitle = "";
125
+ if (col === "percent") {
126
+ paddedTitle = headerTitle.padStart(width);
127
+ } else {
128
+ paddedTitle = headerTitle.padEnd(width);
129
+ }
130
+ segment = colorize(paddedTitle, "bold", useColor);
131
+
132
+ // Create separator line using hyphens
133
+ sep = colorize("-".repeat(width), "dim", useColor);
134
+
135
+ headerSegments.push(segment);
136
+ separatorSegments.push(sep);
137
+ }
138
+
139
+ outputRows.push({ id: "header", providerName: "header", line: headerSegments.join(" ") });
140
+ outputRows.push({ id: "sep", providerName: "sep", line: separatorSegments.join(" ") });
141
+ }
142
+
143
+ // 4. Render lines
144
+ rows.forEach((row) => {
145
+ const segments: string[] = [];
146
+
147
+ for (let i = 0; i < columns.length; i++) {
148
+ const col = columns[i];
149
+ const content = row.cells[col];
150
+ const width = widths[col];
151
+
152
+ let segment = "";
153
+ const visibleLength = content.replace(/\x1b\[[0-9;]*m/g, "").length;
154
+ const padding = Math.max(0, width - visibleLength);
155
+
156
+ if (col === "percent") {
157
+ segment = " ".repeat(padding) + content;
158
+ } else {
159
+ segment = content + " ".repeat(padding);
160
+ }
161
+
162
+ segments.push(segment);
163
+ }
164
+
165
+ outputRows.push({
166
+ id: row.quota.id,
167
+ providerName: row.quota.providerName,
168
+ line: segments.join(" "), // Use 3 spaces separator as in example
169
+ });
170
+ });
171
+
172
+ return outputRows;
173
+ }
@@ -0,0 +1 @@
1
+ // Legacy helper removed. Use logger.debug directly.
@@ -0,0 +1,41 @@
1
+ import { homedir, platform } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export function getDataDirectory(): string {
5
+ const home = homedir();
6
+
7
+ switch (platform()) {
8
+ case "win32":
9
+ return process.env.APPDATA
10
+ ? join(process.env.APPDATA, "opencode")
11
+ : join(home, "AppData", "Roaming", "opencode");
12
+ case "darwin":
13
+ return join(home, "Library", "Application Support", "opencode");
14
+ default:
15
+ return process.env.XDG_DATA_HOME
16
+ ? join(process.env.XDG_DATA_HOME, "opencode")
17
+ : join(home, ".local", "share", "opencode");
18
+ }
19
+ }
20
+
21
+ export function getConfigDirectory(): string {
22
+ const home = homedir();
23
+
24
+ switch (platform()) {
25
+ case "win32":
26
+ return process.env.APPDATA
27
+ ? join(process.env.APPDATA, "opencode")
28
+ : join(home, "AppData", "Roaming", "opencode");
29
+ case "darwin":
30
+ return join(home, "Library", "Application Support", "opencode");
31
+ default:
32
+ return process.env.XDG_CONFIG_HOME
33
+ ? join(process.env.XDG_CONFIG_HOME, "opencode")
34
+ : join(home, ".config", "opencode");
35
+ }
36
+ }
37
+
38
+ export const AUTH_FILE = (): string => join(getDataDirectory(), "auth.json");
39
+ export const HISTORY_FILE = (): string => join(getDataDirectory(), "quota-history.json");
40
+ export const DEBUG_LOG_FILE = (): string => join(getDataDirectory(), "quotas-debug.log");
41
+ export const ANTIGRAVITY_ACCOUNTS_FILE = (): string => join(getConfigDirectory(), "antigravity-accounts.json");
@@ -0,0 +1,40 @@
1
+ export function formatRelativeTime(targetDate: Date): string {
2
+ const now = new Date();
3
+ const diffMs = targetDate.getTime() - now.getTime();
4
+ if (diffMs <= 0) return "now";
5
+
6
+ const diffMins = Math.floor(diffMs / (1000 * 60));
7
+ const diffHours = Math.floor(diffMins / 60);
8
+ const diffDays = Math.floor(diffHours / 24);
9
+ const remainingHours = diffHours % 24;
10
+ const remainingMins = diffMins % 60;
11
+
12
+ if (diffDays > 0) {
13
+ return `${diffDays}d ${remainingHours}h`;
14
+ }
15
+ if (diffHours > 0) {
16
+ return `${diffHours}h ${remainingMins}m`;
17
+ }
18
+ return `${diffMins}m`;
19
+ }
20
+
21
+ export function formatDurationMs(ms: number): string {
22
+ if (ms <= 0) return "now";
23
+
24
+ const diffMins = Math.floor(ms / (1000 * 60));
25
+ const diffHours = Math.floor(diffMins / 60);
26
+ const diffDays = Math.floor(diffHours / 24);
27
+ const remainingHours = diffHours % 24;
28
+ const remainingMins = diffMins % 60;
29
+
30
+ if (diffDays > 0) {
31
+ return `${diffDays}d ${remainingHours}h`;
32
+ }
33
+ if (diffHours > 0) {
34
+ return `${diffHours}h ${remainingMins}m`;
35
+ }
36
+ if (diffMins > 0) {
37
+ return `${diffMins}m`;
38
+ }
39
+ return "less than 1m";
40
+ }
@@ -0,0 +1,63 @@
1
+ import { type QuotaData } from "../interfaces";
2
+
3
+ export function isValidNumber(v: unknown): v is number {
4
+ return typeof v === "number" && Number.isFinite(v);
5
+ }
6
+
7
+ export function clamp(value: number, min: number, max?: number): number {
8
+ if (!Number.isFinite(value)) return min;
9
+ if (value < min) return min;
10
+ if (max !== undefined && value > max) return max;
11
+ return value;
12
+ }
13
+
14
+ export function validatePollingInterval(v: unknown): number | null {
15
+ if (v === null || v === undefined) return null;
16
+ const n = typeof v === "string" ? Number(v.trim()) : Number(v);
17
+ if (!Number.isFinite(n) || n <= 0) return null;
18
+ return n;
19
+ }
20
+
21
+ export function validateQuotaData(input: unknown): QuotaData | null {
22
+ if (typeof input !== "object" || input === null) return null;
23
+ const q = input as Partial<QuotaData>;
24
+
25
+ if (!q.id || typeof q.id !== "string") return null;
26
+ if (!q.providerName || typeof q.providerName !== "string") return null;
27
+
28
+ let used = Number(q.used ?? 0);
29
+ if (!Number.isFinite(used)) used = 0;
30
+ if (used < 0) used = 0;
31
+
32
+ let limit: number | null = null;
33
+ if (q.limit === null) {
34
+ limit = null;
35
+ } else if (q.limit !== undefined) {
36
+ const n = Number(q.limit);
37
+ if (Number.isFinite(n) && n > 0) {
38
+ limit = n;
39
+ } else {
40
+ limit = null;
41
+ }
42
+ }
43
+
44
+ const unit = typeof q.unit === "string" ? q.unit : "";
45
+ const reset = typeof q.reset === "string" ? q.reset : undefined;
46
+ const predictedReset = typeof q.predictedReset === "string" ? q.predictedReset : undefined;
47
+ const window = typeof q.window === "string" ? q.window : undefined;
48
+ const info = typeof q.info === "string" ? q.info : undefined;
49
+ const details = typeof q.details === "string" ? q.details : undefined;
50
+
51
+ return {
52
+ id: q.id,
53
+ providerName: q.providerName,
54
+ used,
55
+ limit,
56
+ unit,
57
+ reset,
58
+ predictedReset,
59
+ window,
60
+ info,
61
+ details,
62
+ };
63
+ }