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.
- package/CHANGELOG.md +24 -0
- package/footer-status.ts +84 -44
- package/harness/auto-quota-resume.ts +15 -12
- package/harness/e2e/minimax-quota-scraper.ts +9 -9
- package/harness/e2e/openai-quota-scraper.ts +442 -0
- package/harness/loop-runtime.ts +11 -7
- package/index.ts +170 -53
- package/package.json +2 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/auth/dist/minimax-browser-auth.d.ts +89 -0
- package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -0
- package/packages/auth/dist/minimax-browser-auth.js +890 -0
- package/packages/auth/dist/minimax-browser-auth.js.map +1 -0
- package/packages/auth/dist/run-minimax-auth.d.ts +19 -0
- package/packages/auth/dist/run-minimax-auth.d.ts.map +1 -0
- package/packages/auth/dist/run-minimax-auth.js +112 -0
- package/packages/auth/dist/run-minimax-auth.js.map +1 -0
- package/packages/auth/package.json +1 -1
- package/packages/auth/tsconfig.json +9 -0
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/dist/provider-id.d.ts +5 -0
- package/packages/providers/dist/provider-id.d.ts.map +1 -1
- package/packages/providers/dist/provider-id.js +6 -1
- package/packages/providers/dist/provider-id.js.map +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/providers/src/provider-id.ts +6 -1
- package/packages/quota-manager/dist/tui-usage-monitor.d.ts.map +1 -1
- package/packages/quota-manager/dist/tui-usage-monitor.js +0 -6
- package/packages/quota-manager/dist/tui-usage-monitor.js.map +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/quota-manager/src/tui-usage-monitor.ts +0 -21
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/dependency-analyzer/dist/analyzer.d.ts +0 -17
- package/packages/dependency-analyzer/dist/analyzer.d.ts.map +0 -1
- package/packages/dependency-analyzer/dist/analyzer.js +0 -106
- package/packages/dependency-analyzer/dist/analyzer.js.map +0 -1
- package/packages/dependency-analyzer/dist/imports.d.ts +0 -12
- package/packages/dependency-analyzer/dist/imports.d.ts.map +0 -1
- package/packages/dependency-analyzer/dist/imports.js +0 -73
- package/packages/dependency-analyzer/dist/imports.js.map +0 -1
- package/packages/dependency-analyzer/dist/index.d.ts +0 -4
- package/packages/dependency-analyzer/dist/index.d.ts.map +0 -1
- package/packages/dependency-analyzer/dist/index.js +0 -3
- package/packages/dependency-analyzer/dist/index.js.map +0 -1
- package/packages/dependency-analyzer/dist/test/analyzer.test.js +0 -37
- package/packages/dependency-analyzer/dist/types.d.ts +0 -28
- package/packages/dependency-analyzer/dist/types.d.ts.map +0 -1
- package/packages/dependency-analyzer/dist/types.js +0 -5
- package/packages/dependency-analyzer/dist/types.js.map +0 -1
- package/packages/intent-analyzer/dist/analyzer.d.ts +0 -13
- package/packages/intent-analyzer/dist/analyzer.d.ts.map +0 -1
- package/packages/intent-analyzer/dist/analyzer.js +0 -72
- package/packages/intent-analyzer/dist/analyzer.js.map +0 -1
- package/packages/intent-analyzer/dist/index.d.ts +0 -3
- package/packages/intent-analyzer/dist/index.d.ts.map +0 -1
- package/packages/intent-analyzer/dist/index.js +0 -2
- package/packages/intent-analyzer/dist/index.js.map +0 -1
- package/packages/intent-analyzer/dist/test/analyzer.test.js +0 -56
- package/packages/intent-analyzer/dist/types.d.ts +0 -26
- package/packages/intent-analyzer/dist/types.d.ts.map +0 -1
- package/packages/intent-analyzer/dist/types.js +0 -7
- package/packages/intent-analyzer/dist/types.js.map +0 -1
- package/packages/workspace-scanner/dist/detect.d.ts +0 -3
- package/packages/workspace-scanner/dist/detect.d.ts.map +0 -1
- package/packages/workspace-scanner/dist/detect.js +0 -58
- package/packages/workspace-scanner/dist/detect.js.map +0 -1
- package/packages/workspace-scanner/dist/git.d.ts +0 -7
- package/packages/workspace-scanner/dist/git.d.ts.map +0 -1
- package/packages/workspace-scanner/dist/git.js +0 -75
- package/packages/workspace-scanner/dist/git.js.map +0 -1
- package/packages/workspace-scanner/dist/index.d.ts +0 -5
- package/packages/workspace-scanner/dist/index.d.ts.map +0 -1
- package/packages/workspace-scanner/dist/index.js +0 -4
- package/packages/workspace-scanner/dist/index.js.map +0 -1
- package/packages/workspace-scanner/dist/scanner.d.ts +0 -11
- package/packages/workspace-scanner/dist/scanner.d.ts.map +0 -1
- package/packages/workspace-scanner/dist/scanner.js +0 -46
- package/packages/workspace-scanner/dist/scanner.js.map +0 -1
- package/packages/workspace-scanner/dist/test/scanner.test.js +0 -34
- package/packages/workspace-scanner/dist/types.d.ts +0 -35
- package/packages/workspace-scanner/dist/types.d.ts.map +0 -1
- package/packages/workspace-scanner/dist/types.js +0 -5
- 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
|