pi-harness-runtime 1.1.80 → 1.1.82

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 (116) hide show
  1. package/harness/browser-scrapers/base-browser-scraper.d.ts +91 -0
  2. package/harness/browser-scrapers/chrome-profile-manager.d.ts +80 -0
  3. package/harness/browser-scrapers/codex-browser-scraper.d.ts +60 -0
  4. package/harness/browser-scrapers/index.d.ts +29 -0
  5. package/harness/browser-scrapers/zai-browser-scraper.d.ts +79 -0
  6. package/harness/e2e/browser-scrapers/base-browser-scraper.d.ts +92 -0
  7. package/harness/e2e/browser-scrapers/base-browser-scraper.ts +244 -0
  8. package/harness/e2e/browser-scrapers/chrome-profile-manager.d.ts +81 -0
  9. package/harness/e2e/browser-scrapers/chrome-profile-manager.ts +297 -0
  10. package/harness/e2e/browser-scrapers/codex-browser-scraper.d.ts +61 -0
  11. package/harness/e2e/browser-scrapers/codex-browser-scraper.ts +343 -0
  12. package/harness/e2e/browser-scrapers/index.d.ts +30 -0
  13. package/harness/e2e/browser-scrapers/index.ts +48 -0
  14. package/harness/e2e/browser-scrapers/zai-browser-scraper.d.ts +80 -0
  15. package/harness/e2e/browser-scrapers/zai-browser-scraper.ts +392 -0
  16. package/harness/e2e/glm-quota-scraper.ts +106 -15
  17. package/harness/e2e/index.ts +29 -0
  18. package/harness/e2e/unified-quota-manager.ts +364 -0
  19. package/index.ts +72 -11
  20. package/mirror.ts +32 -28
  21. package/package.json +1 -1
  22. package/packages/a2a-adapter/package.json +1 -1
  23. package/packages/architecture-generator/package.json +1 -1
  24. package/packages/auth/package.json +1 -1
  25. package/packages/autonomous-refactor/package.json +1 -1
  26. package/packages/autonomous-runtime/package.json +1 -1
  27. package/packages/cache-strategy/package.json +1 -1
  28. package/packages/capability-registry/package.json +1 -1
  29. package/packages/checkpoint/package.json +1 -1
  30. package/packages/cli-plugin-sdk/package.json +1 -1
  31. package/packages/clipboard/package.json +1 -1
  32. package/packages/clipboard-plugin/package.json +1 -1
  33. package/packages/code-generation/package.json +1 -1
  34. package/packages/code-review/package.json +1 -1
  35. package/packages/codex-adapter/package.json +1 -1
  36. package/packages/config-capture/package.json +1 -1
  37. package/packages/context-compiler/package.json +1 -1
  38. package/packages/context-discovery/package.json +1 -1
  39. package/packages/context-manager/package.json +1 -1
  40. package/packages/cookie-sanitizer/package.json +1 -1
  41. package/packages/cost-optimizer/package.json +1 -1
  42. package/packages/dependency-analyzer/package.json +1 -1
  43. package/packages/django-plugin/package.json +1 -1
  44. package/packages/doc-generator/package.json +1 -1
  45. package/packages/evaluation-engine/package.json +1 -1
  46. package/packages/evaluation-runner/package.json +1 -1
  47. package/packages/event-bus/package.json +1 -1
  48. package/packages/event-store/package.json +1 -1
  49. package/packages/experience-replay/package.json +1 -1
  50. package/packages/feedback-collector/package.json +1 -1
  51. package/packages/file-copy-helper/package.json +1 -1
  52. package/packages/framework-detector/package.json +1 -1
  53. package/packages/framework-plugin-sdk/package.json +1 -1
  54. package/packages/frappe-plugin/package.json +1 -1
  55. package/packages/generic-web-plugin/package.json +1 -1
  56. package/packages/health-monitor/package.json +1 -1
  57. package/packages/intent-analyzer/package.json +1 -1
  58. package/packages/jev-judge/package.json +1 -1
  59. package/packages/knowledge-graph/package.json +1 -1
  60. package/packages/knowledge-retrieval/package.json +1 -1
  61. package/packages/laravel-plugin/package.json +1 -1
  62. package/packages/learning-engine/package.json +1 -1
  63. package/packages/mcp-adapter/package.json +1 -1
  64. package/packages/memory-engine/package.json +1 -1
  65. package/packages/milestone-manager/package.json +1 -1
  66. package/packages/model-registry/package.json +1 -1
  67. package/packages/nextjs-plugin/package.json +1 -1
  68. package/packages/notification/package.json +1 -1
  69. package/packages/observability/package.json +1 -1
  70. package/packages/okf-indexer/package.json +1 -1
  71. package/packages/performance-optimizer/package.json +1 -1
  72. package/packages/privilege-broker/package.json +1 -1
  73. package/packages/project-analyzer/package.json +1 -1
  74. package/packages/project-bootstrap/package.json +1 -1
  75. package/packages/projection-engine/package.json +1 -1
  76. package/packages/prompt-compiler/package.json +1 -1
  77. package/packages/prompt-versioning/package.json +1 -1
  78. package/packages/provider-adapter-sdk/package.json +1 -1
  79. package/packages/provider-router/package.json +1 -1
  80. package/packages/provider-selector/package.json +1 -1
  81. package/packages/providers/dist/provider-id.d.ts.map +1 -1
  82. package/packages/providers/dist/provider-id.js +1 -2
  83. package/packages/providers/dist/provider-id.js.map +1 -1
  84. package/packages/providers/package.json +1 -1
  85. package/packages/providers/src/provider-id.ts +1 -2
  86. package/packages/quota-manager/package.json +1 -1
  87. package/packages/rate-limiter/package.json +1 -1
  88. package/packages/react-vite-plugin/package.json +1 -1
  89. package/packages/release-manager/package.json +1 -1
  90. package/packages/requirement-compiler/package.json +1 -1
  91. package/packages/runtime/package.json +1 -1
  92. package/packages/scheduler/package.json +1 -1
  93. package/packages/scheduler-adapter/package.json +1 -1
  94. package/packages/session/package.json +1 -1
  95. package/packages/session-api/package.json +1 -1
  96. package/packages/session-export/package.json +1 -1
  97. package/packages/shared-context/package.json +1 -1
  98. package/packages/skill-mcp-client/package.json +1 -1
  99. package/packages/skill-registry/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/tencentdb-memory/package.json +1 -1
  104. package/packages/tencentdb-sync/package.json +1 -1
  105. package/packages/test-data-generator/package.json +1 -1
  106. package/packages/test-generator/package.json +1 -1
  107. package/packages/todo-bd-sync/package.json +1 -1
  108. package/packages/token-estimation/package.json +1 -1
  109. package/packages/token-optimizer/package.json +1 -1
  110. package/packages/trajectory/package.json +1 -1
  111. package/packages/tui/package.json +1 -1
  112. package/packages/types/package.json +1 -1
  113. package/packages/workflow-events/package.json +1 -1
  114. package/packages/workspace-scanner/package.json +1 -1
  115. package/packages/worktree/package.json +1 -1
  116. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Z.ai Browser Scraper — Scrapes z.ai usage data
3
+ *
4
+ * Authentication approach:
5
+ * - Z.ai uses cookies from browser login (different from ChatGPT)
6
+ * - Cookies from ~/.config/google-chrome or browser export need to be tested
7
+ * - Fallback: Manual cookie export from browser DevTools
8
+ *
9
+ * Note: If cookies don't work for browser automation (like Codex),
10
+ * we may need to find alternative auth methods (OAuth, API keys, etc.)
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+
17
+ import { BaseBrowserScraper, type BrowserScraperConfig } from "./base-browser-scraper.js";
18
+
19
+ export interface ZaiQuotaData {
20
+ provider: "z-ai";
21
+ /** Plan type (e.g., "Pro", "Team", "Enterprise") */
22
+ planType?: string;
23
+ /** Current usage percentage (0-100) */
24
+ usagePercent: number;
25
+ /** Total quota (e.g., "5 GB") */
26
+ totalQuota?: string;
27
+ /** Used quota (e.g., "1.5 GB") */
28
+ usedQuota?: string;
29
+ /** Remaining quota */
30
+ remainingQuota?: string;
31
+ /** Reset date/time */
32
+ resetAt?: string;
33
+ /** Billing period */
34
+ period?: string;
35
+ /** Timestamp */
36
+ scrapedAt: string;
37
+ }
38
+
39
+ interface ZaiScraperConfig {
40
+ provider?: string;
41
+ url?: string;
42
+ headless?: boolean;
43
+ timeout?: number;
44
+ viewport?: { width: number; height: number };
45
+ slowMo?: number;
46
+ /** Path to cookies file (Netscape format) */
47
+ cookiesFile?: string;
48
+ }
49
+
50
+ // Known cookie file locations to try
51
+ const COOKIE_LOCATIONS = [
52
+ // Chrome Linux
53
+ join(homedir(), ".config", "google-chrome", "Default", "Cookies"),
54
+ join(homedir(), ".config", "google-chrome", "Default", "Network", "Cookies"),
55
+ join(homedir(), ".config", "chromium", "Default", "Cookies"),
56
+ // Netscape cookie format exports
57
+ join(homedir(), ".pi-harness-runtime", "cookies", "zai-cookies.txt"),
58
+ join(homedir(), ".config", "zai-cookies.txt"),
59
+ ];
60
+
61
+ /**
62
+ * Z.ai Browser Scraper
63
+ */
64
+ export class ZaiBrowserScraper extends BaseBrowserScraper<ZaiQuotaData> {
65
+ private cookiesFile?: string;
66
+
67
+ constructor(config: Partial<ZaiScraperConfig> = {}) {
68
+ super({
69
+ provider: "z-ai",
70
+ url: config.url ?? "https://z.ai/manage-apikey/coding-plan/personal/usage",
71
+ headless: config.headless ?? true,
72
+ timeout: config.timeout ?? 30000,
73
+ viewport: config.viewport,
74
+ slowMo: config.slowMo,
75
+ });
76
+
77
+ this.cookiesFile = config.cookiesFile ?? this.findCookiesFile();
78
+ }
79
+
80
+ /**
81
+ * Find available cookies file
82
+ */
83
+ private findCookiesFile(): string | undefined {
84
+ for (const location of COOKIE_LOCATIONS) {
85
+ if (existsSync(location)) {
86
+ return location;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ protected async createBrowser(): Promise<void> {
93
+ const { chromium } = await import("playwright");
94
+
95
+ const profileDir = join(homedir(), ".pi-harness-runtime", "browser-profiles", "zai");
96
+
97
+ // Launch with anti-detection args
98
+ this.browser = await chromium.launch({
99
+ headless: this.config.headless,
100
+ slowMo: this.config.slowMo,
101
+ args: [
102
+ `--user-data-dir=${profileDir}`,
103
+ "--disable-blink-features=AutomationControlled",
104
+ "--no-first-run",
105
+ "--no-default-browser-check",
106
+ "--disable-extensions",
107
+ ],
108
+ });
109
+
110
+ this.context = await this.browser.newContext({
111
+ viewport: this.config.viewport,
112
+ // If we have a cookies file, import it
113
+ ...(this.cookiesFile && existsSync(this.cookiesFile)
114
+ ? { extraHTTPHeaders: await this.loadCookiesAsHeaders() }
115
+ : {}),
116
+ });
117
+
118
+ this.page = await this.context.newPage();
119
+ }
120
+
121
+ /**
122
+ * Load cookies from file and convert to headers (fallback auth method)
123
+ * Note: This won't work for all sites - some require full cookie jar
124
+ */
125
+ private async loadCookiesAsHeaders(): Promise<Record<string, string>> {
126
+ if (!this.cookiesFile || !existsSync(this.cookiesFile)) {
127
+ return {};
128
+ }
129
+
130
+ try {
131
+ // Try to parse as Netscape format
132
+ const content = readFileSync(this.cookiesFile, "utf-8");
133
+ const lines = content.split("\n").filter((l) => !l.startsWith("#") && l.trim());
134
+
135
+ // Extract session-relevant cookies
136
+ const cookies: Record<string, string> = {};
137
+ for (const line of lines) {
138
+ const parts = line.split("\t");
139
+ if (parts.length >= 7) {
140
+ const domain = parts[0];
141
+ const name = parts[5];
142
+ const value = parts[6];
143
+
144
+ // Only include z.ai cookies
145
+ if (domain.includes("z.ai") || domain.includes("zcloud")) {
146
+ cookies[name] = value;
147
+ }
148
+ }
149
+ }
150
+
151
+ // Return as Cookie header
152
+ const cookieHeader = Object.entries(cookies)
153
+ .map(([k, v]) => `${k}=${v}`)
154
+ .join("; ");
155
+
156
+ return cookieHeader ? { Cookie: cookieHeader } : {};
157
+ } catch {
158
+ return {};
159
+ }
160
+ }
161
+
162
+ protected async isLoginPage(): Promise<boolean> {
163
+ if (!this.page) return false;
164
+
165
+ const url = this.page.url();
166
+
167
+ // Check for login/ auth URLs
168
+ if (/signin|login|auth|oauth/i.test(url)) {
169
+ return true;
170
+ }
171
+
172
+ // Check for common login indicators
173
+ const loginSelectors = [
174
+ 'button:has-text("Sign in")',
175
+ 'button:has-text("Login")',
176
+ 'input[type="email"]',
177
+ 'input[placeholder*="email" i]',
178
+ 'form[action*="login" i]',
179
+ ];
180
+
181
+ for (const selector of loginSelectors) {
182
+ try {
183
+ const element = await this.page.$(selector);
184
+ if (element != null) {
185
+ return true;
186
+ }
187
+ } catch {
188
+ // Selector might not exist
189
+ }
190
+ }
191
+
192
+ return false;
193
+ }
194
+
195
+ protected async extractData(): Promise<ZaiQuotaData | null> {
196
+ if (!this.page) return null;
197
+
198
+ try {
199
+ // Wait for page to fully load
200
+ await this.page.waitForTimeout(3000);
201
+
202
+ // Extract usage percentage
203
+ const usagePercent = await this.extractUsagePercent();
204
+ if (usagePercent === null) {
205
+ console.log("[z.ai] Could not extract usage percentage");
206
+ return null;
207
+ }
208
+
209
+ // Extract additional data
210
+ const planType = await this.extractPlanType();
211
+ const quotaData = await this.extractQuotaData();
212
+ const resetAt = await this.extractResetDate();
213
+
214
+ return {
215
+ provider: "z-ai",
216
+ usagePercent,
217
+ planType,
218
+ ...quotaData,
219
+ resetAt,
220
+ scrapedAt: new Date().toISOString(),
221
+ };
222
+ } catch (error) {
223
+ console.error(`[z.ai] Extraction error: ${error}`);
224
+ return null;
225
+ }
226
+ }
227
+
228
+ private async extractUsagePercent(): Promise<number | null> {
229
+ if (!this.page) return null;
230
+
231
+ // Try various selectors for usage percentage
232
+ const selectors = [
233
+ // Percentage text
234
+ 'text=/\\d+%/',
235
+ // Progress bar/circle with percentage
236
+ "[class*='progress'] [class*='text']",
237
+ "[class*='usage'] [class*='percent']",
238
+ "[class*='quota'] [class*='percent']",
239
+ // Heading near percentage
240
+ "h1:has-text('Usage') + div",
241
+ "h2:has-text('Usage') + div",
242
+ // Data attributes
243
+ "[data-usage]",
244
+ "[data-percent]",
245
+ "[data-quota]",
246
+ ];
247
+
248
+ for (const selector of selectors) {
249
+ try {
250
+ const element = await this.page.$(selector);
251
+ if (element) {
252
+ const text = await element.textContent();
253
+ const match = text?.match(/(\d+(?:\.\d+)?)\s*%/);
254
+ if (match) {
255
+ const percent = parseFloat(match[1]);
256
+ if (!isNaN(percent) && percent >= 0 && percent <= 100) {
257
+ return percent;
258
+ }
259
+ }
260
+ }
261
+ } catch {
262
+ // Continue to next selector
263
+ }
264
+ }
265
+
266
+ return null;
267
+ }
268
+
269
+ private async extractPlanType(): Promise<string | undefined> {
270
+ if (!this.page) return undefined;
271
+
272
+ const selectors = [
273
+ "[class*='plan']",
274
+ "[class*='tier']",
275
+ 'text=/Pro|Team|Enterprise|Starter/i',
276
+ ];
277
+
278
+ for (const selector of selectors) {
279
+ try {
280
+ const element = await this.page.$(selector);
281
+ if (element) {
282
+ const text = await element.textContent();
283
+ if (text && /Pro|Team|Enterprise|Starter/i.test(text)) {
284
+ return text.trim();
285
+ }
286
+ }
287
+ } catch {
288
+ // Continue
289
+ }
290
+ }
291
+
292
+ return undefined;
293
+ }
294
+
295
+ private async extractQuotaData(): Promise<{
296
+ totalQuota?: string;
297
+ usedQuota?: string;
298
+ remainingQuota?: string;
299
+ }> {
300
+ if (!this.page) return {};
301
+
302
+ const result: { totalQuota?: string; usedQuota?: string; remainingQuota?: string } = {};
303
+
304
+ // Try to find quota amounts like "1.5 GB / 5 GB"
305
+ const selectors = [
306
+ 'text=/\\d+.*(?:GB|MB|TB).*(?:\\/|of).*(?:GB|MB|TB)/i',
307
+ "[class*='quota'] [class*='amount']",
308
+ "[class*='usage'] [class*='amount']",
309
+ ];
310
+
311
+ for (const selector of selectors) {
312
+ try {
313
+ const element = await this.page.$(selector);
314
+ if (element) {
315
+ const text = await element.textContent();
316
+ if (text) {
317
+ // Parse "1.5 GB / 5 GB" format
318
+ const match = text.match(/([\d,.]+)\s*(GB|MB|TB)\s*(?:\/|of)\s*([\d,.]+)\s*(GB|MB|TB)/i);
319
+ if (match) {
320
+ result.usedQuota = `${match[1]} ${match[2]}`;
321
+ result.totalQuota = `${match[3]} ${match[4]}`;
322
+ // Calculate remaining
323
+ const used = parseFloat(match[1].replace(/,/g, ""));
324
+ const total = parseFloat(match[3].replace(/,/g, ""));
325
+ if (!isNaN(used) && !isNaN(total)) {
326
+ const remaining = total - used;
327
+ result.remainingQuota = `${remaining.toFixed(2)} ${match[4]}`;
328
+ }
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ } catch {
334
+ // Continue
335
+ }
336
+ }
337
+
338
+ return result;
339
+ }
340
+
341
+ private async extractResetDate(): Promise<string | undefined> {
342
+ if (!this.page) return undefined;
343
+
344
+ const selectors = [
345
+ 'text=/resets?(?:\\s+(?:at|on|in))?\\s*/i',
346
+ "[class*='reset']",
347
+ "[class*='renews']",
348
+ "[class*='billing'] [class*='date']",
349
+ ];
350
+
351
+ for (const selector of selectors) {
352
+ try {
353
+ const element = await this.page.$(selector);
354
+ if (element) {
355
+ const text = await element.textContent();
356
+ if (text && /\d{4}|resets?|renews|billing/i.test(text)) {
357
+ return text.trim();
358
+ }
359
+ }
360
+ } catch {
361
+ // Continue
362
+ }
363
+ }
364
+
365
+ return undefined;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Quick scrape function
371
+ */
372
+ export async function scrapeZaiQuota(cookiesFile?: string): Promise<ZaiQuotaData | null> {
373
+ const scraper = new ZaiBrowserScraper({ cookiesFile });
374
+ try {
375
+ const result = await scraper.scrape();
376
+ return result.data ?? null;
377
+ } finally {
378
+ await scraper.cleanup();
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Check if z.ai cookies are available
384
+ */
385
+ export function checkZaiCookies(): { available: boolean; path?: string } {
386
+ for (const location of COOKIE_LOCATIONS) {
387
+ if (existsSync(location)) {
388
+ return { available: true, path: location };
389
+ }
390
+ }
391
+ return { available: false };
392
+ }
@@ -44,6 +44,18 @@ export interface GLMQuotaData {
44
44
  hourlyCalls: number[];
45
45
  /** Hourly token usage */
46
46
  hourlyTokens: number[];
47
+ /** 5h window usage percentage (0-100) */
48
+ h5UsedPct: number;
49
+ /** When the 5h window resets (human-readable) */
50
+ h5ResetsAt: string;
51
+ /** Epoch ms when the 5h window resets */
52
+ h5ResetsAtEpoch: number;
53
+ /** Weekly usage percentage (0-100) */
54
+ weeklyUsedPct: number;
55
+ /** When the weekly window resets (human-readable) */
56
+ weeklyResetsAt: string;
57
+ /** Epoch ms when the weekly window resets */
58
+ weeklyResetsAtEpoch: number;
47
59
  /** Raw API response for diagnostics */
48
60
  apiEndpoint?: string;
49
61
  /** Timestamp of scrape */
@@ -141,17 +153,63 @@ export class GLMQuotaScraper {
141
153
  quiet: boolean;
142
154
  };
143
155
 
144
- constructor(config: GLMScraperConfig = {}) {
145
- // Resolve API key: direct value > file > null
146
- const apiKey =
147
- config.apiKey ?? loadApiKey(config.apiKeyFile ?? DEFAULT_API_KEY_FILE);
156
+ constructor(config: GLMScraperConfig = {}) {
157
+ // Resolve API key: direct value > file > null
158
+ const apiKey =
159
+ config.apiKey ?? loadApiKey(config.apiKeyFile ?? DEFAULT_API_KEY_FILE);
148
160
 
149
- this.config = {
150
- apiKeyFile: config.apiKeyFile ?? DEFAULT_API_KEY_FILE,
151
- apiKey: apiKey ?? null,
152
- quiet: config.quiet ?? false,
153
- };
154
- }
161
+ this.config = {
162
+ apiKeyFile: config.apiKeyFile ?? DEFAULT_API_KEY_FILE,
163
+ apiKey: apiKey ?? null,
164
+ quiet: config.quiet ?? false,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Calculate 5h and weekly usage from hourly data
170
+ */
171
+ private calculateHourlyUsage(
172
+ timestamps: string[],
173
+ tokens: number[],
174
+ _calls: number[],
175
+ ): { h5Tokens: number; weeklyTokens: number } {
176
+ const now = new Date();
177
+ const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000);
178
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
179
+
180
+ let h5Tokens = 0;
181
+ let weeklyTokens = 0;
182
+
183
+ for (let i = 0; i < timestamps.length; i++) {
184
+ const ts = timestamps[i];
185
+ if (!ts) continue;
186
+
187
+ // Parse timestamp: "2026-09-14 00:00"
188
+ const hourTime = new Date(ts.replace(" ", "T") + ":00");
189
+
190
+ if (hourTime >= fiveHoursAgo) {
191
+ h5Tokens += tokens[i] ?? 0;
192
+ }
193
+
194
+ if (hourTime >= weekAgo) {
195
+ weeklyTokens += tokens[i] ?? 0;
196
+ }
197
+ }
198
+
199
+ return { h5Tokens, weeklyTokens };
200
+ }
201
+
202
+ /**
203
+ * Find the index of the last hour with usage
204
+ */
205
+ private findLastUsageIndex(timestamps: string[], tokens: number[]): number {
206
+ for (let i = timestamps.length - 1; i >= 0; i--) {
207
+ if ((tokens[i] ?? 0) > 0) {
208
+ return i;
209
+ }
210
+ }
211
+ return -1;
212
+ }
155
213
 
156
214
  /**
157
215
  * Set API key directly
@@ -211,8 +269,9 @@ export class GLMQuotaScraper {
211
269
  const end = endDate ?? new Date();
212
270
  const start = startDate ?? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
213
271
 
214
- const startStr = start.toISOString().replace("T", " ").replace("Z", "");
215
- const endStr = end.toISOString().replace("T", " ").replace("Z", "");
272
+ // Format: yyyy-MM-dd HH:mm:ss (no milliseconds)
273
+ const startStr = start.toISOString().replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
274
+ const endStr = end.toISOString().replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
216
275
 
217
276
  const url = `${API_BASE_URL}${USAGE_ENDPOINT}?startTime=${encodeURIComponent(startStr)}&endTime=${encodeURIComponent(endStr)}`;
218
277
 
@@ -272,6 +331,33 @@ export class GLMQuotaScraper {
272
331
  const totalUsage = d.totalUsage ?? {};
273
332
  const modelSummary = d.modelSummaryList?.[0];
274
333
 
334
+ // Calculate 5h and weekly usage from hourly data
335
+ const { h5Tokens, weeklyTokens } = this.calculateHourlyUsage(d.x_time ?? [], d.tokensUsage ?? [], d.modelCallCount ?? []);
336
+
337
+ // GLM Lite plan limits (Legacy Plan V2):
338
+ // Based on browser comparison: 20% weekly = 29.6M tokens
339
+ // So weekly quota ≈ 148M tokens, 5h quota ≈ 30M tokens
340
+ const H5_TOKEN_LIMIT = parseInt(process.env.GLM_5H_TOKEN_LIMIT ?? "30000000", 10); // 30M tokens per 5h
341
+ const WEEKLY_TOKEN_LIMIT = parseInt(process.env.GLM_WEEKLY_TOKEN_LIMIT ?? "148000000", 10); // 148M tokens per week
342
+
343
+ const h5UsedPct = Math.min(100, (h5Tokens / H5_TOKEN_LIMIT) * 100);
344
+ const weeklyUsedPct = Math.min(100, (weeklyTokens / WEEKLY_TOKEN_LIMIT) * 100);
345
+
346
+ // Calculate reset times
347
+ const now = new Date();
348
+ const h5ResetDurationMs = 5 * 60 * 60 * 1000; // 5 hours
349
+ const weeklyResetDurationMs = 7 * 24 * 60 * 60 * 1000; // 7 days
350
+
351
+ // Find last usage time to calculate when 5h window will reset
352
+ const lastUsageIdx = this.findLastUsageIndex(d.x_time ?? [], d.tokensUsage ?? []);
353
+ const lastUsageTime = lastUsageIdx >= 0 && d.x_time ? new Date(d.x_time[lastUsageIdx].replace(" ", "T") + ":00") : now;
354
+ const h5ResetAt = new Date(lastUsageTime.getTime() + h5ResetDurationMs);
355
+ const weeklyResetAt = new Date(now.getTime() + weeklyResetDurationMs);
356
+
357
+ // Calculate seconds until reset
358
+ const h5SecondsUntilReset = Math.max(0, Math.floor((h5ResetAt.getTime() - now.getTime()) / 1000));
359
+ const weeklySecondsUntilReset = Math.max(0, Math.floor((weeklyResetAt.getTime() - now.getTime()) / 1000));
360
+
275
361
  const result: GLMQuotaData = {
276
362
  provider: "glm",
277
363
  totalCalls: totalUsage.totalModelCallCount ?? 0,
@@ -280,15 +366,20 @@ export class GLMQuotaScraper {
280
366
  hourlyTimestamps: d.x_time ?? [],
281
367
  hourlyCalls: d.modelCallCount ?? [],
282
368
  hourlyTokens: d.tokensUsage ?? [],
369
+ h5UsedPct,
370
+ h5ResetsAt: formatRemainsSeconds(h5SecondsUntilReset),
371
+ h5ResetsAtEpoch: h5ResetAt.getTime(),
372
+ weeklyUsedPct,
373
+ weeklyResetsAt: formatRemainsSeconds(weeklySecondsUntilReset),
374
+ weeklyResetsAtEpoch: weeklyResetAt.getTime(),
283
375
  apiEndpoint: url,
284
376
  scrapedAt: new Date().toISOString(),
285
377
  };
286
378
 
287
379
  if (!this.config.quiet) {
288
380
  console.log(
289
- `[DEBUG GLMQuotaScraper] Total calls: ${result.totalCalls}, ` +
290
- `Total tokens: ${result.totalTokens.toLocaleString()}, ` +
291
- `Model: ${result.modelName}`,
381
+ `[DEBUG GLMQuotaScraper] 5h: ${h5UsedPct.toFixed(1)}% (${(h5Tokens/1e6).toFixed(1)}M tokens), ` +
382
+ `Week: ${weeklyUsedPct.toFixed(1)}% (${(weeklyTokens/1e6).toFixed(1)}M tokens)`,
292
383
  );
293
384
  }
294
385
 
@@ -42,3 +42,32 @@ export type {
42
42
  PlaywrightRunnerConfig,
43
43
  QuotaPageData,
44
44
  } from "./playwright-runner.js";
45
+
46
+ // Unified quota management
47
+ export {
48
+ UnifiedQuotaManager,
49
+ createUnifiedQuotaManagerFromEnv,
50
+ type QuotaData,
51
+ type Provider,
52
+ } from "./unified-quota-manager.js";
53
+
54
+ // Individual scrapers for direct access
55
+ export { MiniMaxQuotaManager, type MiniMaxQuotaData } from "./minimax-quota-scraper.js";
56
+ export { GLMQuotaManager, GLMQuotaScraper, type GLMQuotaData, type GLMSubscriptionData, parseGLMErrorResetTime, parseMinimaxOverloadResetTime } from "./glm-quota-scraper.js";
57
+ export { ChatGPTQuotaManager, ChatGPTQuotaScraper, type ChatGPTQuotaData } from "./chatgpt-quota-scraper.js";
58
+ export { OpenAIQuotaManager, OpenAIQuotaScraper, type OpenAIQuotaData } from "./openai-quota-scraper.js";
59
+
60
+ // Browser-based scrapers
61
+ export {
62
+ ChromeProfileManager,
63
+ getChromeProfileManager,
64
+ BaseBrowserScraper,
65
+ CodexBrowserScraper,
66
+ ZaiBrowserScraper,
67
+ scrapeCodexQuota,
68
+ scrapeZaiQuota,
69
+ checkZaiCookies,
70
+ type BrowserScraperConfig,
71
+ type CodexQuotaData,
72
+ type ZaiQuotaData,
73
+ } from "./browser-scrapers/index.js";