pi-harness-runtime 0.9.21 → 0.9.25

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 (152) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/footer-status.ts +84 -44
  3. package/harness/auto-quota-resume.ts +15 -12
  4. package/harness/e2e/minimax-quota-scraper.ts +9 -9
  5. package/harness/e2e/openai-quota-scraper.ts +442 -0
  6. package/harness/loop-runtime.ts +11 -7
  7. package/index.ts +170 -53
  8. package/package.json +2 -2
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/auth/dist/minimax-browser-auth.d.ts +89 -0
  11. package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -0
  12. package/packages/auth/dist/minimax-browser-auth.js +890 -0
  13. package/packages/auth/dist/minimax-browser-auth.js.map +1 -0
  14. package/packages/auth/dist/run-minimax-auth.d.ts +19 -0
  15. package/packages/auth/dist/run-minimax-auth.d.ts.map +1 -0
  16. package/packages/auth/dist/run-minimax-auth.js +112 -0
  17. package/packages/auth/dist/run-minimax-auth.js.map +1 -0
  18. package/packages/auth/package.json +1 -1
  19. package/packages/auth/tsconfig.json +9 -0
  20. package/packages/autonomous-refactor/package.json +1 -1
  21. package/packages/autonomous-runtime/package.json +1 -1
  22. package/packages/cache-strategy/package.json +1 -1
  23. package/packages/capability-registry/package.json +1 -1
  24. package/packages/checkpoint/package.json +1 -1
  25. package/packages/cli-plugin-sdk/package.json +1 -1
  26. package/packages/clipboard/package.json +1 -1
  27. package/packages/code-generation/package.json +1 -1
  28. package/packages/code-review/package.json +1 -1
  29. package/packages/codex-adapter/package.json +1 -1
  30. package/packages/context-compiler/package.json +1 -1
  31. package/packages/context-discovery/package.json +1 -1
  32. package/packages/context-manager/package.json +1 -1
  33. package/packages/cookie-sanitizer/package.json +1 -1
  34. package/packages/cost-optimizer/package.json +1 -1
  35. package/packages/dependency-analyzer/package.json +1 -1
  36. package/packages/django-plugin/package.json +1 -1
  37. package/packages/doc-generator/package.json +1 -1
  38. package/packages/evaluation-engine/package.json +1 -1
  39. package/packages/evaluation-runner/package.json +1 -1
  40. package/packages/event-bus/package.json +1 -1
  41. package/packages/event-store/package.json +1 -1
  42. package/packages/experience-replay/package.json +1 -1
  43. package/packages/feedback-collector/package.json +1 -1
  44. package/packages/framework-detector/package.json +1 -1
  45. package/packages/framework-plugin-sdk/package.json +1 -1
  46. package/packages/frappe-plugin/package.json +1 -1
  47. package/packages/generic-web-plugin/package.json +1 -1
  48. package/packages/health-monitor/package.json +1 -1
  49. package/packages/intent-analyzer/package.json +1 -1
  50. package/packages/laravel-plugin/package.json +1 -1
  51. package/packages/learning-engine/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/performance-optimizer/package.json +1 -1
  59. package/packages/privilege-broker/package.json +1 -1
  60. package/packages/project-analyzer/package.json +1 -1
  61. package/packages/projection-engine/package.json +1 -1
  62. package/packages/prompt-compiler/package.json +1 -1
  63. package/packages/prompt-versioning/package.json +1 -1
  64. package/packages/provider-adapter-sdk/package.json +1 -1
  65. package/packages/provider-router/package.json +1 -1
  66. package/packages/provider-selector/package.json +1 -1
  67. package/packages/providers/dist/provider-id.d.ts +5 -0
  68. package/packages/providers/dist/provider-id.d.ts.map +1 -1
  69. package/packages/providers/dist/provider-id.js +6 -1
  70. package/packages/providers/dist/provider-id.js.map +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/providers/src/provider-id.ts +6 -1
  73. package/packages/quota-manager/dist/tui-usage-monitor.d.ts.map +1 -1
  74. package/packages/quota-manager/dist/tui-usage-monitor.js +0 -6
  75. package/packages/quota-manager/dist/tui-usage-monitor.js.map +1 -1
  76. package/packages/quota-manager/package.json +1 -1
  77. package/packages/quota-manager/src/tui-usage-monitor.ts +0 -21
  78. package/packages/rate-limiter/package.json +1 -1
  79. package/packages/react-vite-plugin/package.json +1 -1
  80. package/packages/release-manager/package.json +1 -1
  81. package/packages/requirement-compiler/package.json +1 -1
  82. package/packages/runtime/package.json +1 -1
  83. package/packages/scheduler/package.json +1 -1
  84. package/packages/scheduler-adapter/package.json +1 -1
  85. package/packages/session/package.json +1 -1
  86. package/packages/session-api/package.json +1 -1
  87. package/packages/session-export/package.json +1 -1
  88. package/packages/shared-context/package.json +1 -1
  89. package/packages/skill-registry/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/test-data-generator/package.json +1 -1
  94. package/packages/test-generator/package.json +1 -1
  95. package/packages/token-estimation/package.json +1 -1
  96. package/packages/token-optimizer/package.json +1 -1
  97. package/packages/tui/package.json +1 -1
  98. package/packages/types/package.json +1 -1
  99. package/packages/workflow-events/package.json +1 -1
  100. package/packages/workspace-scanner/package.json +1 -1
  101. package/packages/worktree/package.json +1 -1
  102. package/packages/dependency-analyzer/dist/analyzer.d.ts +0 -17
  103. package/packages/dependency-analyzer/dist/analyzer.d.ts.map +0 -1
  104. package/packages/dependency-analyzer/dist/analyzer.js +0 -106
  105. package/packages/dependency-analyzer/dist/analyzer.js.map +0 -1
  106. package/packages/dependency-analyzer/dist/imports.d.ts +0 -12
  107. package/packages/dependency-analyzer/dist/imports.d.ts.map +0 -1
  108. package/packages/dependency-analyzer/dist/imports.js +0 -73
  109. package/packages/dependency-analyzer/dist/imports.js.map +0 -1
  110. package/packages/dependency-analyzer/dist/index.d.ts +0 -4
  111. package/packages/dependency-analyzer/dist/index.d.ts.map +0 -1
  112. package/packages/dependency-analyzer/dist/index.js +0 -3
  113. package/packages/dependency-analyzer/dist/index.js.map +0 -1
  114. package/packages/dependency-analyzer/dist/test/analyzer.test.js +0 -37
  115. package/packages/dependency-analyzer/dist/types.d.ts +0 -28
  116. package/packages/dependency-analyzer/dist/types.d.ts.map +0 -1
  117. package/packages/dependency-analyzer/dist/types.js +0 -5
  118. package/packages/dependency-analyzer/dist/types.js.map +0 -1
  119. package/packages/intent-analyzer/dist/analyzer.d.ts +0 -13
  120. package/packages/intent-analyzer/dist/analyzer.d.ts.map +0 -1
  121. package/packages/intent-analyzer/dist/analyzer.js +0 -72
  122. package/packages/intent-analyzer/dist/analyzer.js.map +0 -1
  123. package/packages/intent-analyzer/dist/index.d.ts +0 -3
  124. package/packages/intent-analyzer/dist/index.d.ts.map +0 -1
  125. package/packages/intent-analyzer/dist/index.js +0 -2
  126. package/packages/intent-analyzer/dist/index.js.map +0 -1
  127. package/packages/intent-analyzer/dist/test/analyzer.test.js +0 -56
  128. package/packages/intent-analyzer/dist/types.d.ts +0 -26
  129. package/packages/intent-analyzer/dist/types.d.ts.map +0 -1
  130. package/packages/intent-analyzer/dist/types.js +0 -7
  131. package/packages/intent-analyzer/dist/types.js.map +0 -1
  132. package/packages/workspace-scanner/dist/detect.d.ts +0 -3
  133. package/packages/workspace-scanner/dist/detect.d.ts.map +0 -1
  134. package/packages/workspace-scanner/dist/detect.js +0 -58
  135. package/packages/workspace-scanner/dist/detect.js.map +0 -1
  136. package/packages/workspace-scanner/dist/git.d.ts +0 -7
  137. package/packages/workspace-scanner/dist/git.d.ts.map +0 -1
  138. package/packages/workspace-scanner/dist/git.js +0 -75
  139. package/packages/workspace-scanner/dist/git.js.map +0 -1
  140. package/packages/workspace-scanner/dist/index.d.ts +0 -5
  141. package/packages/workspace-scanner/dist/index.d.ts.map +0 -1
  142. package/packages/workspace-scanner/dist/index.js +0 -4
  143. package/packages/workspace-scanner/dist/index.js.map +0 -1
  144. package/packages/workspace-scanner/dist/scanner.d.ts +0 -11
  145. package/packages/workspace-scanner/dist/scanner.d.ts.map +0 -1
  146. package/packages/workspace-scanner/dist/scanner.js +0 -46
  147. package/packages/workspace-scanner/dist/scanner.js.map +0 -1
  148. package/packages/workspace-scanner/dist/test/scanner.test.js +0 -34
  149. package/packages/workspace-scanner/dist/types.d.ts +0 -35
  150. package/packages/workspace-scanner/dist/types.d.ts.map +0 -1
  151. package/packages/workspace-scanner/dist/types.js +0 -5
  152. package/packages/workspace-scanner/dist/types.js.map +0 -1
@@ -0,0 +1,890 @@
1
+ /**
2
+ * minimax-browser-auth.ts
3
+ *
4
+ * MiniMax browser authentication using a real Chrome profile.
5
+ *
6
+ * SECURITY RULES:
7
+ * - Human owns authentication. Agent never receives credentials.
8
+ * - No username, password, raw cookies, or session tokens stored.
9
+ * - Real Chrome owns the login flow via a persistent profile.
10
+ * - Profile is stored at ~/.pi-harness-runtime/browser-profiles/minimax/
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import * as os from "node:os";
15
+ import * as net from "node:net";
16
+ import { fileURLToPath } from "node:url";
17
+ import { createRequire } from "node:module";
18
+ import { execFileSync, spawn } from "node:child_process";
19
+ import { createInterface } from "node:readline/promises";
20
+ import { chromium, } from "playwright";
21
+ // ESM __dirname shim (not available in ESM without this)
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const _require = createRequire(import.meta.url);
25
+ const DEFAULT_USAGE_KEYWORDS = [
26
+ "Usage",
27
+ "Token",
28
+ "Plan",
29
+ "Credits",
30
+ "Reset",
31
+ "Limit",
32
+ "5h",
33
+ "Weekly",
34
+ "quota",
35
+ "used",
36
+ "coding_plan",
37
+ "subscription",
38
+ ];
39
+ export function getRuntimeDir() {
40
+ return path.join(os.homedir(), ".pi-harness-runtime");
41
+ }
42
+ export function getProfileDir() {
43
+ return path.join(getRuntimeDir(), "browser-profiles", "minimax");
44
+ }
45
+ export function getStatusPath() {
46
+ return path.join(getRuntimeDir(), "auth", "minimax-auth-status.json");
47
+ }
48
+ export function getLiveSessionPath() {
49
+ return path.join(getRuntimeDir(), "auth", "minimax-live-browser.json");
50
+ }
51
+ function ensureDirs(config) {
52
+ const profileDir = config.profilePath ?? getProfileDir();
53
+ const statusPath = config.statusPath ?? getStatusPath();
54
+ fs.mkdirSync(path.dirname(profileDir), { recursive: true });
55
+ fs.mkdirSync(path.dirname(statusPath), { recursive: true });
56
+ // Also ensure the cookie drop folder exists
57
+ fs.mkdirSync(path.join(getRuntimeDir(), "cookies"), { recursive: true });
58
+ }
59
+ export function saveAuthStatus(status, config) {
60
+ const statusPath = config?.statusPath ?? getStatusPath();
61
+ ensureDirs(config ?? {});
62
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
63
+ }
64
+ function loadSavedAuthStatus(config) {
65
+ const statusPath = config?.statusPath ?? getStatusPath();
66
+ if (!fs.existsSync(statusPath)) {
67
+ return null;
68
+ }
69
+ try {
70
+ return JSON.parse(fs.readFileSync(statusPath, "utf-8"));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ function saveLiveBrowserSession(session, liveSessionPath) {
77
+ const p = liveSessionPath ?? getLiveSessionPath();
78
+ fs.mkdirSync(path.dirname(p), { recursive: true });
79
+ fs.writeFileSync(p, JSON.stringify(session, null, 2));
80
+ }
81
+ export function loadLiveBrowserSession(liveSessionPath) {
82
+ const p = liveSessionPath ?? getLiveSessionPath();
83
+ if (!fs.existsSync(p)) {
84
+ return null;
85
+ }
86
+ try {
87
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ function clearLiveBrowserSession(liveSessionPath) {
94
+ const p = liveSessionPath ?? getLiveSessionPath();
95
+ if (fs.existsSync(p)) {
96
+ fs.rmSync(p, { force: true });
97
+ }
98
+ }
99
+ export function detectUsagePage(bodyText) {
100
+ const words = DEFAULT_USAGE_KEYWORDS.map((k) => k.toLowerCase());
101
+ const lowerText = bodyText.toLowerCase();
102
+ const foundKeywords = words.filter((w) => lowerText.includes(w));
103
+ const detected = foundKeywords.length >= 2;
104
+ const sample = detected
105
+ ? bodyText.substring(0, 200).replace(/\s+/g, " ").trim()
106
+ : null;
107
+ return { detected, sample };
108
+ }
109
+ const DEFAULT_AUTH_TIMEOUT_MS = 300000;
110
+ function delay(ms) {
111
+ return new Promise((resolve) => setTimeout(resolve, ms));
112
+ }
113
+ function hasProfileData(profileDir) {
114
+ if (!fs.existsSync(profileDir)) {
115
+ return false;
116
+ }
117
+ const markers = [
118
+ path.join(profileDir, "Local State"),
119
+ path.join(profileDir, "First Run"),
120
+ path.join(profileDir, "Default", "Preferences"),
121
+ path.join(profileDir, "Default", "Cookies"),
122
+ path.join(profileDir, "Default", "History"),
123
+ ];
124
+ if (markers.some((marker) => fs.existsSync(marker))) {
125
+ return true;
126
+ }
127
+ try {
128
+ return fs.readdirSync(profileDir).length > 0;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ function getChromeExecutablePath(config = {}) {
135
+ const configuredPath = config.chromeExecutablePath ??
136
+ process.env.PI_HARNESS_CHROME_PATH ??
137
+ process.env.GOOGLE_CHROME_BIN ??
138
+ process.env.CHROME_PATH;
139
+ const candidates = configuredPath ? [configuredPath] : [];
140
+ switch (process.platform) {
141
+ case "darwin":
142
+ candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta");
143
+ break;
144
+ case "win32":
145
+ candidates.push("C:Program FilesGoogleChromeApplicationchrome.exe", "C:Program Files (x86)GoogleChromeApplicationchrome.exe", path.join(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe"));
146
+ break;
147
+ default:
148
+ candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/google-chrome");
149
+ }
150
+ for (const candidate of candidates) {
151
+ if (candidate && fs.existsSync(candidate)) {
152
+ return candidate;
153
+ }
154
+ }
155
+ throw new Error("Google Chrome executable not found. Install Google Chrome or set PI_HARNESS_CHROME_PATH.");
156
+ }
157
+ function getChromeArgs() {
158
+ const args = ["--no-first-run", "--no-default-browser-check"];
159
+ if (process.platform === "linux") {
160
+ args.push("--password-store=basic");
161
+ }
162
+ return args;
163
+ }
164
+ function getAuthTimeoutMs(config) {
165
+ const configuredTimeout = config.authTimeoutMs ??
166
+ (process.env.PI_HARNESS_AUTH_TIMEOUT_MS
167
+ ? Number(process.env.PI_HARNESS_AUTH_TIMEOUT_MS)
168
+ : undefined);
169
+ if (typeof configuredTimeout === "number" &&
170
+ Number.isFinite(configuredTimeout) &&
171
+ configuredTimeout > 0) {
172
+ return configuredTimeout;
173
+ }
174
+ return DEFAULT_AUTH_TIMEOUT_MS;
175
+ }
176
+ async function getFreePort(preferredPort) {
177
+ if (preferredPort)
178
+ return preferredPort;
179
+ return new Promise((resolve, reject) => {
180
+ const server = net.createServer();
181
+ server.unref();
182
+ server.on("error", reject);
183
+ server.listen(0, "127.0.0.1", () => {
184
+ const address = server.address();
185
+ if (!address || typeof address === "string") {
186
+ server.close();
187
+ reject(new Error("Could not allocate a Chrome debugging port"));
188
+ return;
189
+ }
190
+ const { port } = address;
191
+ server.close((closeError) => {
192
+ if (closeError) {
193
+ reject(closeError);
194
+ return;
195
+ }
196
+ resolve(port);
197
+ });
198
+ });
199
+ });
200
+ }
201
+ async function connectToChromeOverCdp(port, timeoutMs = 15000) {
202
+ const endpoint = `http://127.0.0.1:${port}`;
203
+ const deadline = Date.now() + timeoutMs;
204
+ let lastError;
205
+ while (Date.now() < deadline) {
206
+ try {
207
+ return await chromium.connectOverCDP(endpoint);
208
+ }
209
+ catch (error) {
210
+ lastError = error;
211
+ await delay(500);
212
+ }
213
+ }
214
+ throw new Error(`Could not connect to Chrome DevTools at ${endpoint}: ${String(lastError)}`);
215
+ }
216
+ function getCurrentPage(context) {
217
+ const pages = context.pages().filter((p) => !p.isClosed());
218
+ return pages.length > 0 ? pages[pages.length - 1] : null;
219
+ }
220
+ async function waitForPage(context, timeoutMs = 15000) {
221
+ const deadline = Date.now() + timeoutMs;
222
+ while (Date.now() < deadline) {
223
+ const page = getCurrentPage(context);
224
+ if (page)
225
+ return page;
226
+ await delay(250);
227
+ }
228
+ throw new Error("Chrome launched but no page became available");
229
+ }
230
+ async function closeBrowserResources(browser, chromeProcess) {
231
+ if (browser)
232
+ await browser.close().catch(() => { });
233
+ if (chromeProcess &&
234
+ chromeProcess.exitCode === null &&
235
+ !chromeProcess.killed) {
236
+ chromeProcess.kill("SIGTERM");
237
+ }
238
+ }
239
+ async function launchChromeForAuthentication(profileDir, targetUrl, config) {
240
+ const chromePath = getChromeExecutablePath(config);
241
+ const cdpPort = await getFreePort(config.cdpPort);
242
+ const chromeArgs = [
243
+ `--remote-debugging-port=${cdpPort}`,
244
+ `--user-data-dir=${profileDir}`,
245
+ "--new-window",
246
+ ...getChromeArgs(),
247
+ targetUrl,
248
+ ];
249
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
250
+ const chromeProcess = spawn(chromePath, chromeArgs, { stdio: "ignore" });
251
+ const browser = await connectToChromeOverCdp(cdpPort);
252
+ const context = browser.contexts()[0];
253
+ if (!context) {
254
+ await closeBrowserResources(browser, chromeProcess);
255
+ throw new Error("Connected to Chrome, but no browser context was available");
256
+ }
257
+ return { browser, chromeProcess, chromePath, cdpPort };
258
+ }
259
+ /**
260
+ * Manual authentication flow: launches real Chrome so Google login works,
261
+ * waits for user to sign in, then confirms at the TTY.
262
+ */
263
+ export async function authenticateWithPersistentBrowser(config = {}) {
264
+ const profileDir = config.profilePath ?? getProfileDir();
265
+ const targetUrl = config.targetUrl ?? "https://platform.minimax.io/console/usage";
266
+ ensureDirs(config);
267
+ console.log("=".repeat(60));
268
+ console.log("🔐 MiniMax Browser Authentication (Persistent Mode)");
269
+ console.log("=".repeat(60));
270
+ console.log("");
271
+ console.log("SECURITY: Human owns authentication.");
272
+ console.log("- Login once, profile is saved for reuse");
273
+ console.log("");
274
+ console.log("Profile directory: " + profileDir);
275
+ console.log("");
276
+ let browser = null;
277
+ let chromeProcess = null;
278
+ try {
279
+ const isFirstRun = !hasProfileData(profileDir);
280
+ console.log("🚀 Launching real Google Chrome...");
281
+ if (isFirstRun) {
282
+ console.log(" First run - creating new Chrome profile");
283
+ }
284
+ else {
285
+ console.log(" Reusing existing Chrome profile");
286
+ }
287
+ const launched = await launchChromeForAuthentication(profileDir, targetUrl, config);
288
+ browser = launched.browser;
289
+ chromeProcess = launched.chromeProcess;
290
+ const context = browser.contexts()[0];
291
+ console.log(" Chrome: " + launched.chromePath);
292
+ console.log(" DevTools port: " + launched.cdpPort);
293
+ console.log("");
294
+ console.log("🖥️ Waiting for MiniMax / Google login flow...");
295
+ let page = await waitForPage(context, 15000);
296
+ await page
297
+ .waitForLoadState("domcontentloaded", { timeout: 15000 })
298
+ .catch(() => { });
299
+ let loginComplete = false;
300
+ let lastLoggedUrl = "";
301
+ let loginHintShown = false;
302
+ const startTime = Date.now();
303
+ const authTimeoutMs = getAuthTimeoutMs(config);
304
+ while (Date.now() - startTime < authTimeoutMs) {
305
+ page = getCurrentPage(context) ?? page;
306
+ if (!page || page.isClosed()) {
307
+ console.log("Browser closed by user.");
308
+ const status = {
309
+ provider: "minimax",
310
+ authenticated: false,
311
+ checked_at: new Date().toISOString(),
312
+ page_url: "",
313
+ detected_text_sample: null,
314
+ profile_path: profileDir,
315
+ error_message: "Browser closed by user",
316
+ };
317
+ saveAuthStatus(status, config);
318
+ return status;
319
+ }
320
+ const currentUrl = page.url();
321
+ if (currentUrl && currentUrl !== lastLoggedUrl) {
322
+ console.log(" 🔗 URL: " + currentUrl);
323
+ lastLoggedUrl = currentUrl;
324
+ }
325
+ const isGoogleLogin = currentUrl.includes("accounts.google.com");
326
+ const isMiniMaxLogin = currentUrl.includes("unified-login") || currentUrl.includes("login");
327
+ if ((isGoogleLogin || isMiniMaxLogin) && !loginHintShown) {
328
+ console.log("");
329
+ console.log("🔓 Sign in in the Chrome window that just opened");
330
+ console.log(" This uses real Chrome so Google login is allowed");
331
+ console.log(" Close browser or press Ctrl+C to cancel");
332
+ console.log("");
333
+ loginHintShown = true;
334
+ }
335
+ const bodyText = (await page.textContent("body").catch(() => "")) ?? "";
336
+ const { detected } = detectUsagePage(bodyText);
337
+ if (detected) {
338
+ loginComplete = true;
339
+ break;
340
+ }
341
+ await delay(1500);
342
+ }
343
+ if (!loginComplete) {
344
+ console.log("");
345
+ console.log("⏰ Browser stayed open after timeout. Please close it.");
346
+ console.log("");
347
+ await waitForChromeToExit(chromeProcess);
348
+ const status = {
349
+ provider: "minimax",
350
+ authenticated: false,
351
+ checked_at: new Date().toISOString(),
352
+ page_url: "",
353
+ detected_text_sample: null,
354
+ profile_path: profileDir,
355
+ error_message: "Login not completed within timeout",
356
+ };
357
+ saveAuthStatus(status, config);
358
+ browser = null;
359
+ chromeProcess = null;
360
+ return status;
361
+ }
362
+ // Usage page detected — wait for human to close the browser
363
+ console.log("");
364
+ console.log("✅ Usage page reached!");
365
+ console.log("💾 Profile is being saved automatically.");
366
+ console.log("");
367
+ console.log("⚠️ Please close the Chrome window now.");
368
+ console.log("");
369
+ await waitForChromeToExit(chromeProcess);
370
+ const confirmed = await confirmUsagePageReached(30000);
371
+ const status = {
372
+ provider: "minimax",
373
+ authenticated: confirmed,
374
+ checked_at: new Date().toISOString(),
375
+ page_url: targetUrl,
376
+ detected_text_sample: confirmed ? "Usage page confirmed by user" : null,
377
+ profile_path: profileDir,
378
+ };
379
+ saveAuthStatus(status, config);
380
+ console.log("");
381
+ if (confirmed) {
382
+ console.log("✅ Authentication saved! Future scrapes will use this profile.");
383
+ }
384
+ else {
385
+ console.log("⚠️ Auth not confirmed. Run 'auth' again after login.");
386
+ }
387
+ browser = null;
388
+ chromeProcess = null;
389
+ return status;
390
+ }
391
+ catch (error) {
392
+ const errorMessage = error instanceof Error ? error.message : String(error);
393
+ console.error("❌ Error: " + errorMessage);
394
+ await closeBrowserResources(browser, chromeProcess);
395
+ const status = {
396
+ provider: "minimax",
397
+ authenticated: false,
398
+ checked_at: new Date().toISOString(),
399
+ page_url: "",
400
+ detected_text_sample: null,
401
+ profile_path: profileDir,
402
+ error_message: errorMessage,
403
+ };
404
+ saveAuthStatus(status, config);
405
+ return status;
406
+ }
407
+ }
408
+ async function waitForChromeToExit(chromeProcess) {
409
+ return new Promise((resolve) => {
410
+ // macOS: helper processes keep chrome running even after window closes
411
+ // Use process group to kill all related processes
412
+ const timeout = setTimeout(() => {
413
+ console.log(" (timeout waiting for Chrome exit — killing process group)");
414
+ try {
415
+ process.kill(chromeProcess.pid, "SIGTERM");
416
+ }
417
+ catch (_e) {
418
+ /* ignore */
419
+ }
420
+ resolve();
421
+ }, 15000);
422
+ chromeProcess.once("exit", () => {
423
+ clearTimeout(timeout);
424
+ resolve();
425
+ });
426
+ // Also resolve on Enter (TTY fallback)
427
+ if (process.stdin.isTTY) {
428
+ console.log(" Press ENTER after closing Chrome...");
429
+ process.stdin.once("data", () => {
430
+ clearTimeout(timeout);
431
+ resolve();
432
+ });
433
+ }
434
+ });
435
+ }
436
+ async function confirmUsagePageReached(_timeoutMs = 30000) {
437
+ if (!process.stdin.isTTY) {
438
+ console.log("(non-TTY: auto-confirming)");
439
+ return true;
440
+ }
441
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
442
+ const answer = await rl.question("✅ Did you reach the MiniMax usage page? (y/N): ");
443
+ rl.close();
444
+ return answer.trim().toLowerCase() === "y";
445
+ }
446
+ /**
447
+ * Extract usage-relevant lines from page body text.
448
+ * Splits on newlines AND HTML tag boundaries, then filters for keywords.
449
+ */
450
+ export function extractUsageLines(bodyText) {
451
+ // Strip <script> JSON noise first
452
+ let clean = bodyText.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
453
+ clean = clean.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
454
+ clean = clean.replace(/\{\s*"[^{}]*\}/g, "");
455
+ // Split on both newlines and HTML tag boundaries
456
+ const htmlTagRe = /\n|<div[^>]*>|<\/div>|<span[^>]*>|<\/span>|<p[^>]*>|<\/p>|<br\s*\/?>|\s{2,}/gi;
457
+ // Replace tag matches with spaces so adjacent text doesn't concatenate
458
+ clean = clean.replace(htmlTagRe, " ");
459
+ const parts = clean.split(/\s+/);
460
+ const lines = [];
461
+ const keywords = DEFAULT_USAGE_KEYWORDS.map((k) => k.toLowerCase());
462
+ for (const part of parts) {
463
+ const trimmed = part.trim();
464
+ if (trimmed.length < 3 || trimmed.length > 200)
465
+ continue;
466
+ const lower = trimmed.toLowerCase();
467
+ const hasKeyword = keywords.some((k) => lower.includes(k));
468
+ const hasNumber = /\d/.test(trimmed);
469
+ const hasPercent = /%|\d+\/\d+/.test(trimmed);
470
+ if (hasKeyword || (hasNumber && hasPercent)) {
471
+ // Deduplicate
472
+ const normalized = trimmed.replace(/\s+/g, " ");
473
+ if (!lines.includes(normalized)) {
474
+ lines.push(normalized);
475
+ }
476
+ }
477
+ }
478
+ return lines;
479
+ }
480
+ /**
481
+ * Scrape usage data by attaching to a live browser session (via Node CDP).
482
+ * Writes attach script to a temp .js file to avoid shell escaping issues.
483
+ */
484
+ export async function scrapeViaLiveBrowserSession(config = {}) {
485
+ const quiet = config.quiet === true;
486
+ const log = (...args) => {
487
+ if (!quiet)
488
+ console.log(...args);
489
+ };
490
+ const errorLog = (...args) => {
491
+ if (!quiet)
492
+ console.error(...args);
493
+ };
494
+ const liveSession = loadLiveBrowserSession();
495
+ if (!liveSession) {
496
+ return {
497
+ provider: "minimax",
498
+ authenticated: false,
499
+ checked_at: new Date().toISOString(),
500
+ page_url: "",
501
+ detected_text_sample: null,
502
+ profile_path: config.profilePath ?? getProfileDir(),
503
+ error_message: "No live browser session. Run 'open' first.",
504
+ };
505
+ }
506
+ const profileDir = liveSession.profile_path;
507
+ const targetUrl = "https://platform.minimax.io/console/usage";
508
+ const port = liveSession.debugging_port;
509
+ log("Attaching to live Chrome session (port " + port + ")...");
510
+ log("Profile: " + profileDir);
511
+ // Resolve playwright from the project root so Node can find it in the temp script.
512
+ // Walk up from this file's location to find the project root (has node_modules).
513
+ let projectRoot = __dirname;
514
+ while (projectRoot !== "/" &&
515
+ projectRoot !== "" &&
516
+ !fs.existsSync(path.join(projectRoot, "node_modules", "playwright"))) {
517
+ projectRoot = path.dirname(projectRoot);
518
+ }
519
+ if (!fs.existsSync(path.join(projectRoot, "node_modules", "playwright"))) {
520
+ // Fallback: try CWD
521
+ projectRoot = process.cwd();
522
+ }
523
+ const playwrightPath = _require.resolve("playwright", {
524
+ paths: [projectRoot],
525
+ });
526
+ // Build attach script as a plain string (no template literals with regex)
527
+ const scriptLines = [
528
+ "const { chromium } = require('" +
529
+ playwrightPath.replace(/\\/g, "\\\\") +
530
+ "');",
531
+ "(async () => {",
532
+ " const browser = await chromium.connectOverCDP('http://127.0.0.1:" +
533
+ port +
534
+ "');",
535
+ " const context = browser.contexts()[0];",
536
+ " if (!context) { console.error('NO_CONTEXT'); process.exit(1); }",
537
+ " const pages = context.pages().filter(p => !p.isClosed());",
538
+ " const page = pages.length > 0 ? pages[pages.length - 1] : await context.newPage();",
539
+ " await page.goto('" +
540
+ targetUrl +
541
+ "', { waitUntil: 'domcontentloaded', timeout: 30000 });",
542
+ " await page.waitForTimeout(3000);",
543
+ " const url = page.url();",
544
+ " const bodyText = await page.locator('body').innerText({ timeout: 10000 }).catch(() => '');",
545
+ " const result = JSON.stringify({ url, bodyText });",
546
+ " console.log('RESULT:' + result);",
547
+ " await browser.close();",
548
+ "})().catch(e => { console.error('ERROR:' + e.message); process.exit(1); });",
549
+ ];
550
+ const scriptContent = scriptLines.join("\n");
551
+ // Write to temp file to avoid shell escaping issues
552
+ const tmpDir = os.tmpdir();
553
+ const scriptPath = path.join(tmpDir, "pi-harness-minimax-attach-" + Date.now() + ".js");
554
+ fs.writeFileSync(scriptPath, scriptContent);
555
+ try {
556
+ const raw = execFileSync("node", [scriptPath], {
557
+ encoding: "utf-8",
558
+ timeout: 60000,
559
+ stdio: ["ignore", "pipe", "pipe"],
560
+ }).trim();
561
+ fs.unlinkSync(scriptPath);
562
+ const resultMatch = raw.match(/RESULT:(.+)/s);
563
+ if (!resultMatch) {
564
+ throw new Error("No RESULT from attach script: " + raw.substring(0, 200));
565
+ }
566
+ const parsed = JSON.parse(resultMatch[1]);
567
+ const bodyText = parsed.bodyText ?? "";
568
+ const url = parsed.url ?? "";
569
+ const { detected, sample } = detectUsagePage(bodyText);
570
+ const usage_lines = extractUsageLines(bodyText);
571
+ log("URL: " + url);
572
+ log("Detected usage page: " + detected);
573
+ if (usage_lines.length > 0) {
574
+ log("Usage lines (" + usage_lines.length + "):");
575
+ usage_lines.slice(0, 20).forEach((l) => log(" " + l));
576
+ }
577
+ const status = {
578
+ provider: "minimax",
579
+ authenticated: detected,
580
+ checked_at: new Date().toISOString(),
581
+ page_url: url,
582
+ detected_text_sample: sample,
583
+ profile_path: profileDir,
584
+ usage_lines,
585
+ };
586
+ saveAuthStatus(status, config);
587
+ return status;
588
+ }
589
+ catch (error) {
590
+ try {
591
+ fs.unlinkSync(scriptPath);
592
+ }
593
+ catch (_e) {
594
+ /* ignore */
595
+ }
596
+ const errorMessage = error instanceof Error ? error.message : String(error);
597
+ errorLog("Attach failed: " + errorMessage);
598
+ return {
599
+ provider: "minimax",
600
+ authenticated: false,
601
+ checked_at: new Date().toISOString(),
602
+ page_url: "",
603
+ detected_text_sample: null,
604
+ profile_path: profileDir,
605
+ error_message: "Live attach failed: " + errorMessage,
606
+ };
607
+ }
608
+ }
609
+ /**
610
+ * Start a persistent browser daemon (keep-open mode).
611
+ * Launches real Chrome with --remote-debugging-port and saves session.
612
+ */
613
+ export async function startPersistentBrowserDaemon(config = {}) {
614
+ const profileDir = config.profilePath ?? getProfileDir();
615
+ const targetUrl = config.targetUrl ?? "https://platform.minimax.io/console/usage";
616
+ ensureDirs(config);
617
+ // Kill any existing daemon for this profile
618
+ await stopPersistentBrowserDaemon(config);
619
+ const chromePath = getChromeExecutablePath(config);
620
+ const cdpPort = await getFreePort(config.cdpPort ?? 9222);
621
+ const chromeArgs = [
622
+ "--remote-debugging-port=" + cdpPort,
623
+ "--user-data-dir=" + profileDir,
624
+ "--new-window",
625
+ ...getChromeArgs(),
626
+ targetUrl,
627
+ ];
628
+ console.log("Launching persistent Chrome daemon...");
629
+ console.log(" Chrome: " + chromePath);
630
+ console.log(" Port: " + cdpPort);
631
+ console.log(" Profile: " + profileDir);
632
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
633
+ const chromeProcess = spawn(chromePath, chromeArgs, {
634
+ stdio: "ignore",
635
+ detached: true,
636
+ });
637
+ const session = {
638
+ profile_path: profileDir,
639
+ target_url: targetUrl,
640
+ chrome_path: chromePath,
641
+ debugging_port: cdpPort,
642
+ pid: chromeProcess.pid,
643
+ started_at: new Date().toISOString(),
644
+ };
645
+ saveLiveBrowserSession(session);
646
+ console.log("Live session saved. PID: " + chromeProcess.pid);
647
+ return session;
648
+ }
649
+ /**
650
+ * Stop the persistent browser daemon.
651
+ */
652
+ export async function stopPersistentBrowserDaemon(_config = {}) {
653
+ const liveSession = loadLiveBrowserSession(getLiveSessionPath());
654
+ if (!liveSession)
655
+ return false;
656
+ console.log("Stopping live Chrome session (PID " + liveSession.pid + ")...");
657
+ const killed = [];
658
+ // Try process group kill
659
+ try {
660
+ process.kill(-liveSession.pid, "SIGTERM");
661
+ killed.push("process group " + liveSession.pid);
662
+ }
663
+ catch (_e) {
664
+ /* ignore */
665
+ }
666
+ // Try direct PID
667
+ try {
668
+ process.kill(liveSession.pid, "SIGTERM");
669
+ killed.push("PID " + liveSession.pid);
670
+ }
671
+ catch (_e) {
672
+ /* ignore */
673
+ }
674
+ // Kill by profile path (macOS helper processes)
675
+ try {
676
+ const out = execFileSync("pgrep", ["-f", "user-data-dir=" + liveSession.profile_path], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
677
+ for (const pid of out.trim().split("\n")) {
678
+ try {
679
+ process.kill(parseInt(pid), "SIGTERM");
680
+ killed.push("profile PID " + pid);
681
+ }
682
+ catch (_e2) {
683
+ /* ignore */
684
+ }
685
+ }
686
+ }
687
+ catch (_e) {
688
+ /* ignore */
689
+ }
690
+ clearLiveBrowserSession();
691
+ console.log("Stopped: " + (killed.length > 0 ? killed.join(", ") : "nothing found"));
692
+ return true;
693
+ }
694
+ /**
695
+ * Get active live browser session info.
696
+ */
697
+ export function getActiveLiveBrowserSession() {
698
+ return loadLiveBrowserSession();
699
+ }
700
+ /**
701
+ * Scrape usage data. Tries live session first, falls back to headless Playwright.
702
+ */
703
+ export async function scrapeWithExistingProfile(config = {}) {
704
+ const profileDir = config.profilePath ?? getProfileDir();
705
+ const targetUrl = config.targetUrl ?? "https://platform.minimax.io/console/usage";
706
+ ensureDirs(config);
707
+ const quiet = config.quiet === true;
708
+ const log = (...args) => {
709
+ if (!quiet)
710
+ console.log(...args);
711
+ };
712
+ const errorLog = (...args) => {
713
+ if (!quiet)
714
+ console.error(...args);
715
+ };
716
+ log("=".repeat(60));
717
+ log("📊 MiniMax Usage Scraper");
718
+ log("=".repeat(60));
719
+ log("");
720
+ // 1. Try live session first (skip if forced off for testing)
721
+ const liveSession = config.forceNoLiveSession
722
+ ? null
723
+ : loadLiveBrowserSession();
724
+ if (liveSession) {
725
+ log("Live browser session found (PID " + liveSession.pid + ")");
726
+ log("Trying live attach...");
727
+ const result = await scrapeViaLiveBrowserSession(config);
728
+ if (result.authenticated || result.usage_lines) {
729
+ log("✅ Live session scrape successful!");
730
+ return result;
731
+ }
732
+ // Live session exists but attach failed — do NOT fall back to headless
733
+ // because that would try to lock the same profile (already locked by the daemon)
734
+ log("⚠️ Live session exists but attach failed.");
735
+ log(" Make sure your Chrome window is still open.");
736
+ log(" To stop and restart: bun packages/auth/src/run-minimax-auth.ts stop");
737
+ return {
738
+ provider: "minimax",
739
+ authenticated: false,
740
+ checked_at: new Date().toISOString(),
741
+ page_url: "",
742
+ detected_text_sample: null,
743
+ profile_path: liveSession.profile_path,
744
+ error_message: "Live attach failed: " +
745
+ (result.error_message ?? "unknown error") +
746
+ ". Is the Chrome window still open?",
747
+ };
748
+ }
749
+ // 2. Fall back to headless Playwright with existing profile
750
+ const profileExists = hasProfileData(profileDir);
751
+ if (!profileExists) {
752
+ log("⚠️ No profile found. Run auth first:");
753
+ log(" bun packages/auth/src/run-minimax-auth.ts auth");
754
+ return {
755
+ provider: "minimax",
756
+ authenticated: false,
757
+ checked_at: new Date().toISOString(),
758
+ page_url: "",
759
+ detected_text_sample: null,
760
+ profile_path: profileDir,
761
+ error_message: "No profile found - run auth first",
762
+ };
763
+ }
764
+ log("Launching headless browser with saved profile...");
765
+ let context = null;
766
+ try {
767
+ context = await chromium.launchPersistentContext(profileDir, {
768
+ executablePath: getChromeExecutablePath(config),
769
+ headless: true,
770
+ chromiumSandbox: false,
771
+ args: getChromeArgs(),
772
+ viewport: { width: 1280, height: 800 },
773
+ });
774
+ const page = context.pages()[0] ?? (await context.newPage());
775
+ log("Navigating to " + targetUrl + "...");
776
+ await page.goto(targetUrl, {
777
+ waitUntil: "domcontentloaded",
778
+ timeout: 30000,
779
+ });
780
+ await page.waitForTimeout(3000);
781
+ const url = page.url();
782
+ const bodyText = await page
783
+ .locator("body")
784
+ .innerText({ timeout: 10000 })
785
+ .catch(() => "");
786
+ const { detected, sample } = detectUsagePage(bodyText);
787
+ const usage_lines = extractUsageLines(bodyText);
788
+ // Check if redirected to login
789
+ const redirectedToLogin = url.includes("unified-login") || url.includes("login");
790
+ const savedStatus = loadSavedAuthStatus(config);
791
+ const wasPreviouslyAuthenticated = savedStatus?.authenticated ?? false;
792
+ let authenticated = detected;
793
+ let error_message;
794
+ if (redirectedToLogin && wasPreviouslyAuthenticated) {
795
+ log("⚠️ Redirected to login but previously authenticated. Profile may have expired.");
796
+ authenticated = false;
797
+ error_message = "Session expired - redirected to login";
798
+ }
799
+ log("URL: " + url);
800
+ log("Detected usage page: " + detected);
801
+ if (usage_lines.length > 0) {
802
+ log("Usage lines (" + usage_lines.length + "):");
803
+ usage_lines.slice(0, 20).forEach((l) => log(" " + l));
804
+ }
805
+ const status = {
806
+ provider: "minimax",
807
+ authenticated,
808
+ checked_at: new Date().toISOString(),
809
+ page_url: url,
810
+ detected_text_sample: sample,
811
+ profile_path: profileDir,
812
+ usage_lines,
813
+ error_message,
814
+ };
815
+ saveAuthStatus(status, config);
816
+ await context.close();
817
+ context = null;
818
+ return status;
819
+ }
820
+ catch (error) {
821
+ const errorMessage = error instanceof Error ? error.message : String(error);
822
+ errorLog("❌ Error: " + errorMessage);
823
+ if (context)
824
+ await context.close().catch(() => { });
825
+ const status = {
826
+ provider: "minimax",
827
+ authenticated: false,
828
+ checked_at: new Date().toISOString(),
829
+ page_url: "",
830
+ detected_text_sample: null,
831
+ profile_path: profileDir,
832
+ error_message: errorMessage,
833
+ };
834
+ saveAuthStatus(status, config);
835
+ return status;
836
+ }
837
+ }
838
+ /**
839
+ * Check auth status.
840
+ */
841
+ export async function checkAuthStatus(config = {}) {
842
+ const statusPath = config.statusPath ?? getStatusPath();
843
+ const profileDir = config.profilePath ?? getProfileDir();
844
+ console.log("Checking MiniMax authentication status...");
845
+ if (fs.existsSync(statusPath)) {
846
+ const content = fs.readFileSync(statusPath, "utf-8");
847
+ try {
848
+ const status = JSON.parse(content);
849
+ console.log("");
850
+ console.log(status.authenticated
851
+ ? "✅ User is logged in"
852
+ : "⚠️ User is NOT logged in");
853
+ if (status.usage_lines && status.usage_lines.length > 0) {
854
+ console.log("Usage lines available: " + status.usage_lines.length);
855
+ }
856
+ console.log("Checked at: " + status.checked_at);
857
+ console.log("Profile: " + status.profile_path);
858
+ return status;
859
+ }
860
+ catch {
861
+ console.log("⚠️ Status file is corrupted");
862
+ }
863
+ }
864
+ else {
865
+ console.log("⚠️ No status file found. Run auth first.");
866
+ }
867
+ const liveSession = loadLiveBrowserSession();
868
+ if (liveSession) {
869
+ console.log("Live browser session active (PID " +
870
+ liveSession.pid +
871
+ ", port " +
872
+ liveSession.debugging_port +
873
+ ")");
874
+ }
875
+ if (!hasProfileData(profileDir)) {
876
+ console.log("⚠️ No Chrome profile found at " + profileDir);
877
+ }
878
+ else {
879
+ console.log("✅ Chrome profile exists at " + profileDir);
880
+ }
881
+ return {
882
+ provider: "minimax",
883
+ authenticated: false,
884
+ checked_at: new Date().toISOString(),
885
+ page_url: "",
886
+ detected_text_sample: null,
887
+ profile_path: profileDir,
888
+ };
889
+ }
890
+ //# sourceMappingURL=minimax-browser-auth.js.map