pi-harness-runtime 1.1.81 → 1.1.83

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 +90 -1
  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,91 @@
1
+ /**
2
+ * Base Browser Scraper — Abstract class for browser-based quota scraping
3
+ *
4
+ * Uses Playwright with Chrome profiles to scrape JavaScript-rendered pages
5
+ * that require authentication. Similar approach to google-flow-mcp.
6
+ */
7
+ import type { Page, Browser, BrowserContext } from "playwright";
8
+ export interface BrowserScraperConfig {
9
+ /** Headless mode (default: true) */
10
+ headless?: boolean;
11
+ /** Page load timeout in ms (default: 30000) */
12
+ timeout?: number;
13
+ /** Viewport dimensions */
14
+ viewport?: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ /** Slow down operations by ms (for debugging) */
19
+ slowMo?: number;
20
+ /** Provider name for logging */
21
+ provider: string;
22
+ /** URL to scrape */
23
+ url: string;
24
+ }
25
+ export interface QuotaScrapingResult<T> {
26
+ /** Whether scraping was successful */
27
+ success: boolean;
28
+ /** The scraped data */
29
+ data?: T;
30
+ /** Error message if failed */
31
+ error?: string;
32
+ /** Timestamp of scrape */
33
+ scrapedAt: string;
34
+ /** Screenshot path if debug screenshot was taken */
35
+ screenshotPath?: string;
36
+ }
37
+ /**
38
+ * Abstract base class for browser-based quota scrapers
39
+ */
40
+ export declare abstract class BaseBrowserScraper<T> {
41
+ protected config: Required<BrowserScraperConfig>;
42
+ protected browser: Browser | null;
43
+ protected context: BrowserContext | null;
44
+ protected page: Page | null;
45
+ protected screenshotsEnabled: boolean;
46
+ protected screenshotsDir: string;
47
+ constructor(config: BrowserScraperConfig);
48
+ /**
49
+ * Initialize Playwright and launch browser
50
+ */
51
+ protected abstract createBrowser(): Promise<void>;
52
+ /**
53
+ * Take a debug screenshot
54
+ */
55
+ takeScreenshot(name: string): Promise<string | undefined>;
56
+ /**
57
+ * Wait for a selector with retry logic
58
+ */
59
+ protected waitForSelector(selector: string, options?: {
60
+ timeout?: number;
61
+ state?: "attached" | "detached" | "visible" | "hidden";
62
+ }): Promise<boolean>;
63
+ /**
64
+ * Extract text content from page
65
+ */
66
+ protected extractText(selector: string): Promise<string | null>;
67
+ /**
68
+ * Extract number from text (handles various formats like "1.5 GB", "$10.00", etc.)
69
+ */
70
+ protected extractNumber(text: string | null, pattern?: RegExp): number | null;
71
+ /**
72
+ * Main scraping method — navigates and extracts data
73
+ */
74
+ scrape(): Promise<QuotaScrapingResult<T>>;
75
+ /**
76
+ * Check if current page is a login page
77
+ */
78
+ protected abstract isLoginPage(): Promise<boolean>;
79
+ /**
80
+ * Extract quota data from the page
81
+ */
82
+ protected abstract extractData(): Promise<T | null>;
83
+ /**
84
+ * Cleanup browser resources
85
+ */
86
+ cleanup(): Promise<void>;
87
+ /**
88
+ * Enable screenshot capture for debugging
89
+ */
90
+ enableScreenshots(dir?: string): void;
91
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Chrome Profile Manager — Browser-based quota scraping via Playwright
3
+ *
4
+ * Key concepts from google-flow-mcp:
5
+ * - Uses Playwright to connect to existing Chrome profile with stored cookies
6
+ * - Maintains persistent session across runs
7
+ * - Bypasses anti-bot measures by launching Chrome directly
8
+ *
9
+ * This enables scraping JavaScript-rendered pages that require authentication:
10
+ * - chatgpt.com/codex/cloud/settings/analytics
11
+ * - z.ai/manage-apikey/coding-plan/personal/usage
12
+ * - etc.
13
+ */
14
+ export interface ChromeProfileConfig {
15
+ /** Provider name (e.g., "codex", "z-ai") */
16
+ provider: string;
17
+ /** Path to Chrome user data directory (auto-detected if not provided) */
18
+ userDataDir?: string;
19
+ /** Profile directory name within userDataDir */
20
+ profileName?: string;
21
+ /** Path to browser binary (auto-detected if not provided) */
22
+ binaryPath?: string;
23
+ /** Arguments to pass to Chrome */
24
+ extraArgs?: string[];
25
+ /** Headless mode (default: true for scraping) */
26
+ headless?: boolean;
27
+ /** Launch timeout in ms */
28
+ launchTimeout?: number;
29
+ }
30
+ export interface BrowserProfile {
31
+ /** Path to the profile directory */
32
+ profilePath: string;
33
+ /** Provider name */
34
+ provider: string;
35
+ /** Whether the profile exists */
36
+ exists: boolean;
37
+ /** Last used timestamp */
38
+ lastUsed?: number;
39
+ }
40
+ /** Auto-detect Chrome user data directory based on OS */
41
+ export declare function detectChromeUserDataDir(): string | null;
42
+ /** Auto-detect Chrome binary path */
43
+ export declare function detectChromeBinary(): string | null;
44
+ /**
45
+ * Chrome Profile Manager for browser-based scraping
46
+ *
47
+ * Manages Chrome profiles for different providers to maintain
48
+ * authenticated sessions across scraping runs.
49
+ */
50
+ export declare class ChromeProfileManager {
51
+ private profilesDir;
52
+ private profiles;
53
+ constructor(profilesDir?: string);
54
+ private ensureProfilesDir;
55
+ private getProfileMetaPath;
56
+ private loadProfiles;
57
+ /**
58
+ * Get profile path for a provider
59
+ */
60
+ getProfilePath(provider: string): string;
61
+ /**
62
+ * Ensure profile directory exists
63
+ */
64
+ ensureProfile(provider: string): BrowserProfile;
65
+ private saveProfileMeta;
66
+ /**
67
+ * List all managed profiles
68
+ */
69
+ listProfiles(): BrowserProfile[];
70
+ /**
71
+ * Delete a profile
72
+ */
73
+ deleteProfile(provider: string): Promise<void>;
74
+ /**
75
+ * Get Chrome launch arguments for anti-bot evasion
76
+ * Based on google-flow-mcp approach
77
+ */
78
+ getLaunchArgs(config: ChromeProfileConfig): string[];
79
+ }
80
+ export declare function getChromeProfileManager(): ChromeProfileManager;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Codex Browser Scraper — Scrapes chatgpt.com/codex/cloud/settings/analytics
3
+ *
4
+ * Uses Playwright with Chrome profile to access the authenticated page
5
+ * and extract usage/quota data.
6
+ */
7
+ import { BaseBrowserScraper } from "./base-browser-scraper.js";
8
+ export interface CodexQuotaData {
9
+ provider: "openai-codex";
10
+ /** Plan type (e.g., "pro", "plus") */
11
+ planType?: string;
12
+ /** Current usage percentage (0-100) */
13
+ usagePercent: number;
14
+ /** Total quota amount */
15
+ totalQuota?: string;
16
+ /** Used quota amount */
17
+ usedQuota?: string;
18
+ /** Remaining quota */
19
+ remainingQuota?: string;
20
+ /** Reset date/time */
21
+ resetAt?: string;
22
+ /** Usage period description */
23
+ period?: string;
24
+ /** Raw data for debugging */
25
+ rawData?: Record<string, unknown>;
26
+ /** Timestamp */
27
+ scrapedAt: string;
28
+ }
29
+ interface CodexScraperConfig {
30
+ provider?: string;
31
+ url?: string;
32
+ headless?: boolean;
33
+ timeout?: number;
34
+ viewport?: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ slowMo?: number;
39
+ }
40
+ /**
41
+ * Codex Browser Scraper
42
+ *
43
+ * Scrapes usage data from chatgpt.com/codex/cloud/settings/analytics
44
+ */
45
+ export declare class CodexBrowserScraper extends BaseBrowserScraper<CodexQuotaData> {
46
+ constructor(config?: Partial<CodexScraperConfig>);
47
+ protected createBrowser(): Promise<void>;
48
+ private getProfileDir;
49
+ protected isLoginPage(): Promise<boolean>;
50
+ protected extractData(): Promise<CodexQuotaData | null>;
51
+ private extractUsageData;
52
+ private extractAdditionalData;
53
+ private extractFromHeadings;
54
+ private extractChartData;
55
+ }
56
+ /**
57
+ * Quick scrape function
58
+ */
59
+ export declare function scrapeCodexQuota(): Promise<CodexQuotaData | null>;
60
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Browser Scrapers — Browser-based quota scraping via Playwright
3
+ *
4
+ * Authentication strategies by provider:
5
+ *
6
+ * | Provider | Auth Method | Cookies Work? | Notes |
7
+ * |-----------|----------------------|---------------|--------------------------|
8
+ * | MiniMax | Browser cookies | Yes | Direct browser scraping |
9
+ * | ChatGPT | OAuth tokens | No | Use ~/.codex/auth.json |
10
+ * | Codex | OAuth tokens | No | Use ~/.codex/auth.json |
11
+ * | Z.ai | Browser cookies | Unknown | Try browser profile |
12
+ *
13
+ * For providers where browser cookies don't work for automation:
14
+ * - Use API tokens (OAuth, API keys)
15
+ * - Export cookies from DevTools (may not work due to HttpOnly, Secure flags)
16
+ * - Manual authentication via headful browser with user interaction
17
+ */
18
+ export { ChromeProfileManager, getChromeProfileManager, detectChromeUserDataDir, detectChromeBinary, type ChromeProfileConfig, type BrowserProfile, } from "./chrome-profile-manager.js";
19
+ export { BaseBrowserScraper, type BrowserScraperConfig, type QuotaScrapingResult, } from "./base-browser-scraper.js";
20
+ export { CodexBrowserScraper, scrapeCodexQuota, type CodexQuotaData } from "./codex-browser-scraper.js";
21
+ export { ZaiBrowserScraper, scrapeZaiQuota, checkZaiCookies, type ZaiQuotaData } from "./zai-browser-scraper.js";
22
+ /**
23
+ * All available browser scrapers
24
+ */
25
+ export declare const BROWSER_SCRAPERS: {
26
+ readonly "openai-codex": () => Promise<typeof import("./codex-browser-scraper.js").CodexBrowserScraper>;
27
+ readonly "z-ai": () => Promise<typeof import("./zai-browser-scraper.js").ZaiBrowserScraper>;
28
+ };
29
+ export type BrowserScraperProvider = keyof typeof BROWSER_SCRAPERS;
@@ -0,0 +1,79 @@
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
+ import { BaseBrowserScraper } from "./base-browser-scraper.js";
13
+ export interface ZaiQuotaData {
14
+ provider: "z-ai";
15
+ /** Plan type (e.g., "Pro", "Team", "Enterprise") */
16
+ planType?: string;
17
+ /** Current usage percentage (0-100) */
18
+ usagePercent: number;
19
+ /** Total quota (e.g., "5 GB") */
20
+ totalQuota?: string;
21
+ /** Used quota (e.g., "1.5 GB") */
22
+ usedQuota?: string;
23
+ /** Remaining quota */
24
+ remainingQuota?: string;
25
+ /** Reset date/time */
26
+ resetAt?: string;
27
+ /** Billing period */
28
+ period?: string;
29
+ /** Timestamp */
30
+ scrapedAt: string;
31
+ }
32
+ interface ZaiScraperConfig {
33
+ provider?: string;
34
+ url?: string;
35
+ headless?: boolean;
36
+ timeout?: number;
37
+ viewport?: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ slowMo?: number;
42
+ /** Path to cookies file (Netscape format) */
43
+ cookiesFile?: string;
44
+ }
45
+ /**
46
+ * Z.ai Browser Scraper
47
+ */
48
+ export declare class ZaiBrowserScraper extends BaseBrowserScraper<ZaiQuotaData> {
49
+ private cookiesFile?;
50
+ constructor(config?: Partial<ZaiScraperConfig>);
51
+ /**
52
+ * Find available cookies file
53
+ */
54
+ private findCookiesFile;
55
+ protected createBrowser(): Promise<void>;
56
+ /**
57
+ * Load cookies from file and convert to headers (fallback auth method)
58
+ * Note: This won't work for all sites - some require full cookie jar
59
+ */
60
+ private loadCookiesAsHeaders;
61
+ protected isLoginPage(): Promise<boolean>;
62
+ protected extractData(): Promise<ZaiQuotaData | null>;
63
+ private extractUsagePercent;
64
+ private extractPlanType;
65
+ private extractQuotaData;
66
+ private extractResetDate;
67
+ }
68
+ /**
69
+ * Quick scrape function
70
+ */
71
+ export declare function scrapeZaiQuota(cookiesFile?: string): Promise<ZaiQuotaData | null>;
72
+ /**
73
+ * Check if z.ai cookies are available
74
+ */
75
+ export declare function checkZaiCookies(): {
76
+ available: boolean;
77
+ path?: string;
78
+ };
79
+ export {};
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Base Browser Scraper — Abstract class for browser-based quota scraping
3
+ *
4
+ * Uses Playwright with Chrome profiles to scrape JavaScript-rendered pages
5
+ * that require authentication. Similar approach to google-flow-mcp.
6
+ */
7
+ import type { Page, Browser, BrowserContext } from "playwright";
8
+ export interface BrowserScraperConfig {
9
+ /** Headless mode (default: true) */
10
+ headless?: boolean;
11
+ /** Page load timeout in ms (default: 30000) */
12
+ timeout?: number;
13
+ /** Viewport dimensions */
14
+ viewport?: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ /** Slow down operations by ms (for debugging) */
19
+ slowMo?: number;
20
+ /** Provider name for logging */
21
+ provider: string;
22
+ /** URL to scrape */
23
+ url: string;
24
+ }
25
+ export interface QuotaScrapingResult<T> {
26
+ /** Whether scraping was successful */
27
+ success: boolean;
28
+ /** The scraped data */
29
+ data?: T;
30
+ /** Error message if failed */
31
+ error?: string;
32
+ /** Timestamp of scrape */
33
+ scrapedAt: string;
34
+ /** Screenshot path if debug screenshot was taken */
35
+ screenshotPath?: string;
36
+ }
37
+ /**
38
+ * Abstract base class for browser-based quota scrapers
39
+ */
40
+ export declare abstract class BaseBrowserScraper<T> {
41
+ protected config: Required<BrowserScraperConfig>;
42
+ protected browser: Browser | null;
43
+ protected context: BrowserContext | null;
44
+ protected page: Page | null;
45
+ protected screenshotsEnabled: boolean;
46
+ protected screenshotsDir: string;
47
+ constructor(config: BrowserScraperConfig);
48
+ /**
49
+ * Initialize Playwright and launch browser
50
+ */
51
+ protected abstract createBrowser(): Promise<void>;
52
+ /**
53
+ * Take a debug screenshot
54
+ */
55
+ takeScreenshot(name: string): Promise<string | undefined>;
56
+ /**
57
+ * Wait for a selector with retry logic
58
+ */
59
+ protected waitForSelector(selector: string, options?: {
60
+ timeout?: number;
61
+ state?: "attached" | "detached" | "visible" | "hidden";
62
+ }): Promise<boolean>;
63
+ /**
64
+ * Extract text content from page
65
+ */
66
+ protected extractText(selector: string): Promise<string | null>;
67
+ /**
68
+ * Extract number from text (handles various formats like "1.5 GB", "$10.00", etc.)
69
+ */
70
+ protected extractNumber(text: string | null, pattern?: RegExp): number | null;
71
+ /**
72
+ * Main scraping method — navigates and extracts data
73
+ */
74
+ scrape(): Promise<QuotaScrapingResult<T>>;
75
+ /**
76
+ * Check if current page is a login page
77
+ */
78
+ protected abstract isLoginPage(): Promise<boolean>;
79
+ /**
80
+ * Extract quota data from the page
81
+ */
82
+ protected abstract extractData(): Promise<T | null>;
83
+ /**
84
+ * Cleanup browser resources
85
+ */
86
+ cleanup(): Promise<void>;
87
+ /**
88
+ * Enable screenshot capture for debugging
89
+ */
90
+ enableScreenshots(dir?: string): void;
91
+ }
92
+ //# sourceMappingURL=base-browser-scraper.d.ts.map
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Base Browser Scraper — Abstract class for browser-based quota scraping
3
+ *
4
+ * Uses Playwright with Chrome profiles to scrape JavaScript-rendered pages
5
+ * that require authentication. Similar approach to google-flow-mcp.
6
+ */
7
+
8
+ import type { Page, Browser, BrowserContext } from "playwright";
9
+
10
+ export interface BrowserScraperConfig {
11
+ /** Headless mode (default: true) */
12
+ headless?: boolean;
13
+ /** Page load timeout in ms (default: 30000) */
14
+ timeout?: number;
15
+ /** Viewport dimensions */
16
+ viewport?: { width: number; height: number };
17
+ /** Slow down operations by ms (for debugging) */
18
+ slowMo?: number;
19
+ /** Provider name for logging */
20
+ provider: string;
21
+ /** URL to scrape */
22
+ url: string;
23
+ }
24
+
25
+ export interface QuotaScrapingResult<T> {
26
+ /** Whether scraping was successful */
27
+ success: boolean;
28
+ /** The scraped data */
29
+ data?: T;
30
+ /** Error message if failed */
31
+ error?: string;
32
+ /** Timestamp of scrape */
33
+ scrapedAt: string;
34
+ /** Screenshot path if debug screenshot was taken */
35
+ screenshotPath?: string;
36
+ }
37
+
38
+ /**
39
+ * Abstract base class for browser-based quota scrapers
40
+ */
41
+ export abstract class BaseBrowserScraper<T> {
42
+ protected config: Required<BrowserScraperConfig>;
43
+ protected browser: Browser | null = null;
44
+ protected context: BrowserContext | null = null;
45
+ protected page: Page | null = null;
46
+ protected screenshotsEnabled = false;
47
+ protected screenshotsDir = "/tmp/pi-harness-screenshots";
48
+
49
+ constructor(config: BrowserScraperConfig) {
50
+ this.config = {
51
+ headless: config.headless ?? true,
52
+ timeout: config.timeout ?? 30000,
53
+ viewport: config.viewport ?? { width: 1280, height: 720 },
54
+ slowMo: config.slowMo ?? 0,
55
+ provider: config.provider,
56
+ url: config.url,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Initialize Playwright and launch browser
62
+ */
63
+ protected abstract createBrowser(): Promise<void>;
64
+
65
+ /**
66
+ * Take a debug screenshot
67
+ */
68
+ async takeScreenshot(name: string): Promise<string | undefined> {
69
+ if (!this.page) return undefined;
70
+
71
+ try {
72
+ const { existsSync, mkdirSync } = require("node:fs");
73
+ const path = require("node:path");
74
+
75
+ if (!existsSync(this.screenshotsDir)) {
76
+ mkdirSync(this.screenshotsDir, { recursive: true });
77
+ }
78
+
79
+ const filePath = path.join(
80
+ this.screenshotsDir,
81
+ `${this.config.provider}-${name}-${Date.now()}.png`,
82
+ );
83
+
84
+ await this.page.screenshot({ path: filePath, fullPage: true });
85
+ console.log(`[${this.config.provider}] Screenshot saved: ${filePath}`);
86
+ return filePath;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Wait for a selector with retry logic
94
+ */
95
+ protected async waitForSelector(
96
+ selector: string,
97
+ options?: { timeout?: number; state?: "attached" | "detached" | "visible" | "hidden" },
98
+ ): Promise<boolean> {
99
+ if (!this.page) return false;
100
+
101
+ try {
102
+ await this.page.waitForSelector(selector, {
103
+ timeout: options?.timeout ?? this.config.timeout,
104
+ state: options?.state ?? "visible",
105
+ });
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Extract text content from page
114
+ */
115
+ protected async extractText(selector: string): Promise<string | null> {
116
+ if (!this.page) return null;
117
+
118
+ try {
119
+ const element = await this.page.$(selector);
120
+ if (!element) return null;
121
+ return await element.textContent();
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Extract number from text (handles various formats like "1.5 GB", "$10.00", etc.)
129
+ */
130
+ protected extractNumber(text: string | null, pattern?: RegExp): number | null {
131
+ if (!text) return null;
132
+
133
+ const cleaned = pattern ? text.match(pattern)?.[0] ?? text : text;
134
+ const match = cleaned.match(/[\d,.]+/);
135
+ if (!match) return null;
136
+
137
+ const num = parseFloat(match[0].replace(/,/g, ""));
138
+ return isNaN(num) ? null : num;
139
+ }
140
+
141
+ /**
142
+ * Main scraping method — navigates and extracts data
143
+ */
144
+ async scrape(): Promise<QuotaScrapingResult<T>> {
145
+ const result: QuotaScrapingResult<T> = {
146
+ success: false,
147
+ scrapedAt: new Date().toISOString(),
148
+ };
149
+
150
+ try {
151
+ // Initialize browser
152
+ await this.createBrowser();
153
+
154
+ if (!this.page) {
155
+ result.error = "Failed to create browser page";
156
+ return result;
157
+ }
158
+
159
+ // Navigate to URL
160
+ console.log(`[${this.config.provider}] Navigating to ${this.config.url}`);
161
+ await this.page.goto(this.config.url, {
162
+ waitUntil: "networkidle",
163
+ timeout: this.config.timeout,
164
+ });
165
+
166
+ // Wait for page to settle
167
+ await this.page.waitForTimeout(2000);
168
+
169
+ // Take screenshot for debugging if enabled
170
+ if (this.screenshotsEnabled) {
171
+ await this.takeScreenshot("initial-load");
172
+ }
173
+
174
+ // Check if login is required
175
+ if (await this.isLoginPage()) {
176
+ result.error = "Login required - browser session not authenticated";
177
+ await this.takeScreenshot("login-required");
178
+ return result;
179
+ }
180
+
181
+ // Extract data using provider-specific logic
182
+ const data = await this.extractData();
183
+ if (!data) {
184
+ result.error = "Failed to extract quota data from page";
185
+ await this.takeScreenshot("extraction-failed");
186
+ return result;
187
+ }
188
+
189
+ result.success = true;
190
+ result.data = data;
191
+
192
+ } catch (error) {
193
+ result.error = error instanceof Error ? error.message : String(error);
194
+ await this.takeScreenshot("error");
195
+
196
+ } finally {
197
+ await this.cleanup();
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ * Check if current page is a login page
205
+ */
206
+ protected abstract isLoginPage(): Promise<boolean>;
207
+
208
+ /**
209
+ * Extract quota data from the page
210
+ */
211
+ protected abstract extractData(): Promise<T | null>;
212
+
213
+ /**
214
+ * Cleanup browser resources
215
+ */
216
+ async cleanup(): Promise<void> {
217
+ try {
218
+ if (this.page) {
219
+ await this.page.close();
220
+ this.page = null;
221
+ }
222
+ if (this.context) {
223
+ await this.context.close();
224
+ this.context = null;
225
+ }
226
+ if (this.browser) {
227
+ await this.browser.close();
228
+ this.browser = null;
229
+ }
230
+ } catch {
231
+ // Ignore cleanup errors
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Enable screenshot capture for debugging
237
+ */
238
+ enableScreenshots(dir?: string): void {
239
+ this.screenshotsEnabled = true;
240
+ if (dir) {
241
+ this.screenshotsDir = dir;
242
+ }
243
+ }
244
+ }