perplexity-user-mcp 0.8.36
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/README.md +192 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.mjs +43 -0
- package/dist/checks/browser.d.ts +100 -0
- package/dist/checks/browser.mjs +89 -0
- package/dist/checks/config.d.ts +91 -0
- package/dist/checks/config.mjs +88 -0
- package/dist/checks/ide.d.ts +89 -0
- package/dist/checks/ide.mjs +80 -0
- package/dist/checks/mcp.d.ts +61 -0
- package/dist/checks/mcp.mjs +56 -0
- package/dist/checks/native-deps.d.ts +131 -0
- package/dist/checks/native-deps.mjs +115 -0
- package/dist/checks/network.d.ts +71 -0
- package/dist/checks/network.mjs +70 -0
- package/dist/checks/probe.d.ts +93 -0
- package/dist/checks/probe.mjs +82 -0
- package/dist/checks/profiles.d.ts +99 -0
- package/dist/checks/profiles.mjs +90 -0
- package/dist/checks/runtime.d.ts +89 -0
- package/dist/checks/runtime.mjs +90 -0
- package/dist/checks/vault.d.ts +101 -0
- package/dist/checks/vault.mjs +90 -0
- package/dist/chunk-3B276PGG.mjs +115 -0
- package/dist/chunk-4UEJOM6W.mjs +9 -0
- package/dist/chunk-6EP2BLTV.mjs +205 -0
- package/dist/chunk-6YMQVLFX.mjs +146 -0
- package/dist/chunk-7JL36EBH.mjs +118 -0
- package/dist/chunk-DPGMKSSA.mjs +57 -0
- package/dist/chunk-H4BUAPPO.mjs +1950 -0
- package/dist/chunk-HMKLWVXB.mjs +109 -0
- package/dist/chunk-HTUAQRKH.mjs +125 -0
- package/dist/chunk-HU5B4FXS.mjs +139 -0
- package/dist/chunk-KCXM2M4B.mjs +1006 -0
- package/dist/chunk-LKJMLGFP.mjs +237 -0
- package/dist/chunk-LZPLNZ5U.mjs +67 -0
- package/dist/chunk-MTDFKNXX.mjs +19 -0
- package/dist/chunk-OF4DMAPJ.mjs +511 -0
- package/dist/chunk-PE23RMXY.mjs +43 -0
- package/dist/chunk-Q2VY4R5F.mjs +175 -0
- package/dist/chunk-S5VD7WTU.mjs +2540 -0
- package/dist/chunk-SVPRB62V.mjs +106 -0
- package/dist/chunk-TQLCLE4L.mjs +345 -0
- package/dist/chunk-U3DGFLXZ.mjs +43 -0
- package/dist/chunk-X45O6YD3.mjs +688 -0
- package/dist/chunk-XKSWCEGI.mjs +168 -0
- package/dist/chunk-Z7DAACGZ.mjs +534 -0
- package/dist/chunk-ZQFUZPLO.mjs +257 -0
- package/dist/cli.d.ts +952 -0
- package/dist/cli.mjs +827 -0
- package/dist/client.d.ts +355 -0
- package/dist/client.mjs +27 -0
- package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
- package/dist/cloud-sync.d.ts +42 -0
- package/dist/cloud-sync.mjs +17 -0
- package/dist/config.d.ts +186 -0
- package/dist/config.mjs +54 -0
- package/dist/daemon/attach.d.ts +36 -0
- package/dist/daemon/attach.mjs +25 -0
- package/dist/daemon/audit.d.ts +23 -0
- package/dist/daemon/audit.mjs +12 -0
- package/dist/daemon/client-http.d.ts +42 -0
- package/dist/daemon/client-http.mjs +29 -0
- package/dist/daemon/index.d.ts +14 -0
- package/dist/daemon/index.mjs +110 -0
- package/dist/daemon/install-tunnel.d.ts +46 -0
- package/dist/daemon/install-tunnel.mjs +14 -0
- package/dist/daemon/launcher.d.ts +163 -0
- package/dist/daemon/launcher.mjs +50 -0
- package/dist/daemon/lockfile.d.ts +29 -0
- package/dist/daemon/lockfile.mjs +18 -0
- package/dist/daemon/server.d.ts +159 -0
- package/dist/daemon/server.mjs +20 -0
- package/dist/daemon/token.d.ts +17 -0
- package/dist/daemon/token.mjs +17 -0
- package/dist/daemon/tunnel-providers/index.d.ts +330 -0
- package/dist/daemon/tunnel-providers/index.mjs +57 -0
- package/dist/daemon/tunnel.d.ts +23 -0
- package/dist/daemon/tunnel.mjs +9 -0
- package/dist/doctor-report.d.ts +24 -0
- package/dist/doctor-report.mjs +14 -0
- package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.mjs +16 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.mjs +15 -0
- package/dist/health-check.d.ts +108 -0
- package/dist/health-check.mjs +92 -0
- package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
- package/dist/history-store.d.ts +65 -0
- package/dist/history-store.mjs +48 -0
- package/dist/impit-login-runner.d.ts +469 -0
- package/dist/impit-login-runner.mjs +685 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.mjs +236 -0
- package/dist/login-runner.d.ts +333 -0
- package/dist/login-runner.mjs +320 -0
- package/dist/logout.d.ts +28 -0
- package/dist/logout.mjs +45 -0
- package/dist/manual-login-runner.d.ts +150 -0
- package/dist/manual-login-runner.mjs +146 -0
- package/dist/native-deps-BNThFHxa.d.ts +175 -0
- package/dist/native-deps-YNKXITRY.mjs +139 -0
- package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
- package/dist/profiles.d.ts +41 -0
- package/dist/profiles.mjs +33 -0
- package/dist/redact.d.ts +159 -0
- package/dist/redact.mjs +11 -0
- package/dist/refresh.d.ts +118 -0
- package/dist/refresh.mjs +21 -0
- package/dist/reinit-watcher.d.ts +15 -0
- package/dist/reinit-watcher.mjs +8 -0
- package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
- package/dist/tty-prompt.d.ts +44 -0
- package/dist/tty-prompt.mjs +39 -0
- package/dist/vault.d-BtRSLZiM.d.ts +8 -0
- package/dist/vault.d.ts +37 -0
- package/dist/vault.mjs +21 -0
- package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
- package/dist/viewer-detect.d.ts +4 -0
- package/dist/viewer-detect.mjs +37 -0
- package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
- package/dist/viewers.d.ts +18 -0
- package/dist/viewers.mjs +122 -0
- package/package.json +152 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/profiles.js
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
5
|
+
var NAME_RE = /^[a-z0-9_-]{1,32}$/;
|
|
6
|
+
function getConfigDir() {
|
|
7
|
+
return process.env.PERPLEXITY_CONFIG_DIR || join(homedir(), ".perplexity-mcp");
|
|
8
|
+
}
|
|
9
|
+
function getProfilesDir() {
|
|
10
|
+
return join(getConfigDir(), "profiles");
|
|
11
|
+
}
|
|
12
|
+
function getProfilePaths(name) {
|
|
13
|
+
const dir = join(getProfilesDir(), name);
|
|
14
|
+
return {
|
|
15
|
+
dir,
|
|
16
|
+
meta: join(dir, "meta.json"),
|
|
17
|
+
vault: join(dir, "vault.enc"),
|
|
18
|
+
vaultPlain: join(dir, "vault.json"),
|
|
19
|
+
browserData: join(dir, "browser-data"),
|
|
20
|
+
modelsCache: join(dir, "models-cache.json"),
|
|
21
|
+
history: join(dir, "history"),
|
|
22
|
+
attachments: join(dir, "attachments"),
|
|
23
|
+
researches: join(dir, "researches"),
|
|
24
|
+
reinit: join(dir, ".reinit")
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function validateName(name) {
|
|
28
|
+
if (!name || typeof name !== "string") return "Profile name is required.";
|
|
29
|
+
if (name.length > 32) return "Profile name must be 32 characters or fewer.";
|
|
30
|
+
if (name !== name.toLowerCase()) return "Profile name must be lowercase.";
|
|
31
|
+
if (!NAME_RE.test(name)) {
|
|
32
|
+
return "Profile name must contain only lowercase letters, digits, hyphens, and underscores. Invalid characters detected.";
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function readMeta(name) {
|
|
37
|
+
const p = getProfilePaths(name).meta;
|
|
38
|
+
if (!existsSync(p)) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function writeMeta(name, meta) {
|
|
46
|
+
const paths = getProfilePaths(name);
|
|
47
|
+
if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
|
|
48
|
+
writeFileSync(paths.meta + ".tmp", JSON.stringify(meta, null, 2) + "\n");
|
|
49
|
+
renameSync(paths.meta + ".tmp", paths.meta);
|
|
50
|
+
}
|
|
51
|
+
function createProfile(name, opts = {}) {
|
|
52
|
+
const err = validateName(name);
|
|
53
|
+
if (err) throw new Error(err);
|
|
54
|
+
const paths = getProfilePaths(name);
|
|
55
|
+
if (existsSync(paths.dir)) throw new Error(`Profile '${name}' already exists.`);
|
|
56
|
+
mkdirSync(paths.dir, { recursive: true });
|
|
57
|
+
const meta = {
|
|
58
|
+
name,
|
|
59
|
+
displayName: opts.displayName ?? name,
|
|
60
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
loginMode: opts.loginMode ?? "manual"
|
|
62
|
+
};
|
|
63
|
+
writeMeta(name, meta);
|
|
64
|
+
return meta;
|
|
65
|
+
}
|
|
66
|
+
function listProfiles() {
|
|
67
|
+
const dir = getProfilesDir();
|
|
68
|
+
if (!existsSync(dir)) return [];
|
|
69
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => readMeta(d.name)).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
function getProfile(name) {
|
|
72
|
+
return readMeta(name);
|
|
73
|
+
}
|
|
74
|
+
function deleteProfile(name) {
|
|
75
|
+
const wasActive = getActiveName() === name;
|
|
76
|
+
const dir = getProfilePaths(name).dir;
|
|
77
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
if (wasActive) {
|
|
79
|
+
const remaining = listProfiles();
|
|
80
|
+
if (remaining.length > 0) {
|
|
81
|
+
setActive(remaining[0].name);
|
|
82
|
+
} else {
|
|
83
|
+
const activePath = getActivePointerPath();
|
|
84
|
+
if (existsSync(activePath)) rmSync(activePath, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
import("./vault.mjs").then((m) => {
|
|
88
|
+
m.__resetKeyCache();
|
|
89
|
+
}).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function getActivePointerPath() {
|
|
93
|
+
return join(getConfigDir(), "active");
|
|
94
|
+
}
|
|
95
|
+
function getActiveName() {
|
|
96
|
+
const p = getActivePointerPath();
|
|
97
|
+
if (!existsSync(p)) return null;
|
|
98
|
+
try {
|
|
99
|
+
const name = readFileSync(p, "utf8").trim();
|
|
100
|
+
return name || null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function getActive() {
|
|
106
|
+
const name = getActiveName();
|
|
107
|
+
if (!name) return null;
|
|
108
|
+
return getProfile(name);
|
|
109
|
+
}
|
|
110
|
+
function setActive(name) {
|
|
111
|
+
if (!getProfile(name)) throw new Error(`Profile '${name}' not found.`);
|
|
112
|
+
const cfg = getConfigDir();
|
|
113
|
+
if (!existsSync(cfg)) mkdirSync(cfg, { recursive: true });
|
|
114
|
+
const p = getActivePointerPath();
|
|
115
|
+
writeFileSync(p + ".tmp", name + "\n");
|
|
116
|
+
renameSync(p + ".tmp", p);
|
|
117
|
+
import("./vault.mjs").then((m) => {
|
|
118
|
+
m.__resetKeyCache();
|
|
119
|
+
}).catch(() => {
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function suggestNextDefaultName() {
|
|
123
|
+
const names = listProfiles().map((p) => p.name);
|
|
124
|
+
let n = 1;
|
|
125
|
+
while (names.includes(`account-${n}`)) n++;
|
|
126
|
+
return `account-${n}`;
|
|
127
|
+
}
|
|
128
|
+
function renameProfile(oldName, newName) {
|
|
129
|
+
const err = validateName(newName);
|
|
130
|
+
if (err) throw new Error(err);
|
|
131
|
+
const oldPaths = getProfilePaths(oldName);
|
|
132
|
+
const newPaths = getProfilePaths(newName);
|
|
133
|
+
if (!existsSync(oldPaths.dir)) throw new Error(`Profile '${oldName}' not found.`);
|
|
134
|
+
if (existsSync(newPaths.dir)) throw new Error(`Profile '${newName}' already exists.`);
|
|
135
|
+
renameSync(oldPaths.dir, newPaths.dir);
|
|
136
|
+
const meta = readMeta(newName);
|
|
137
|
+
if (meta) {
|
|
138
|
+
meta.name = newName;
|
|
139
|
+
if (meta.displayName === oldName) meta.displayName = newName;
|
|
140
|
+
writeMeta(newName, meta);
|
|
141
|
+
}
|
|
142
|
+
if (getActiveName() === oldName) setActive(newName);
|
|
143
|
+
}
|
|
144
|
+
function recordLoginSuccess(name, { tier, loginMode, lastLogin }) {
|
|
145
|
+
const paths = getProfilePaths(name);
|
|
146
|
+
if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
|
|
147
|
+
const current = readMeta(name) ?? { name, displayName: name, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
148
|
+
const meta = { ...current, loginMode, tier, lastLogin };
|
|
149
|
+
writeMeta(name, meta);
|
|
150
|
+
return meta;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
getConfigDir,
|
|
155
|
+
getProfilesDir,
|
|
156
|
+
getProfilePaths,
|
|
157
|
+
validateName,
|
|
158
|
+
createProfile,
|
|
159
|
+
listProfiles,
|
|
160
|
+
getProfile,
|
|
161
|
+
deleteProfile,
|
|
162
|
+
getActiveName,
|
|
163
|
+
getActive,
|
|
164
|
+
setActive,
|
|
165
|
+
suggestNextDefaultName,
|
|
166
|
+
renameProfile,
|
|
167
|
+
recordLoginSuccess
|
|
168
|
+
};
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASI_ACCESS_ENDPOINT,
|
|
3
|
+
EXPERIMENTS_ENDPOINT,
|
|
4
|
+
MODELS_CONFIG_ENDPOINT,
|
|
5
|
+
PERPLEXITY_URL,
|
|
6
|
+
RATE_LIMIT_ENDPOINT,
|
|
7
|
+
USER_INFO_ENDPOINT,
|
|
8
|
+
findBrowser,
|
|
9
|
+
getOrCreateContext,
|
|
10
|
+
getSavedCookies,
|
|
11
|
+
resolveBrowserExecutable
|
|
12
|
+
} from "./chunk-LKJMLGFP.mjs";
|
|
13
|
+
import {
|
|
14
|
+
getActiveName,
|
|
15
|
+
getConfigDir,
|
|
16
|
+
getProfilePaths
|
|
17
|
+
} from "./chunk-XKSWCEGI.mjs";
|
|
18
|
+
|
|
19
|
+
// src/refresh.ts
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from "fs";
|
|
21
|
+
import { dirname, join } from "path";
|
|
22
|
+
function getActiveProfileName() {
|
|
23
|
+
return process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
|
|
24
|
+
}
|
|
25
|
+
function getModelsCacheFile() {
|
|
26
|
+
return getProfilePaths(getActiveProfileName()).modelsCache;
|
|
27
|
+
}
|
|
28
|
+
function getLegacyCookiesFile() {
|
|
29
|
+
return join(getProfilePaths(getActiveProfileName()).dir, "cookies.json");
|
|
30
|
+
}
|
|
31
|
+
function getImpitRuntimeDirPath() {
|
|
32
|
+
return join(getConfigDir(), "native-deps");
|
|
33
|
+
}
|
|
34
|
+
var STEALTH_ARGS = [
|
|
35
|
+
"--disable-blink-features=AutomationControlled",
|
|
36
|
+
// NOTE: `--disable-web-security` was removed (2026-04-27 public-hardening
|
|
37
|
+
// audit). All in-page `fetch()` calls used by the cookie-refresh tier hit
|
|
38
|
+
// the same Perplexity origin, so CORS is not a factor; keeping the flag
|
|
39
|
+
// would needlessly weaken the browser's same-origin policy. The off-origin
|
|
40
|
+
// ASI download path lives in client.ts and now uses APIRequestContext.
|
|
41
|
+
//
|
|
42
|
+
// NOTE: `--disable-features=IsolateOrigins,site-per-process` and
|
|
43
|
+
// `--disable-site-isolation-trials` were removed (2026-04-27 public-
|
|
44
|
+
// hardening audit). They disable Chromium's Site Isolation process model,
|
|
45
|
+
// which is a renderer-architecture feature invisible to JavaScript on the
|
|
46
|
+
// page (no documented fingerprint surface — Patchright's
|
|
47
|
+
// `chromiumSwitches.js` does not include them). The cookie-refresh path
|
|
48
|
+
// never enumerates frames or cross-origin iframes, so the only effect of
|
|
49
|
+
// keeping them was a silent reduction in the browser's Spectre/UXSS
|
|
50
|
+
// defense-in-depth.
|
|
51
|
+
"--no-first-run",
|
|
52
|
+
"--no-default-browser-check",
|
|
53
|
+
"--disable-infobars",
|
|
54
|
+
"--disable-extensions",
|
|
55
|
+
"--disable-popup-blocking"
|
|
56
|
+
];
|
|
57
|
+
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
|
|
58
|
+
var CHROME_CLIENT_HINTS = {
|
|
59
|
+
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
|
|
60
|
+
"sec-ch-ua-mobile": "?0",
|
|
61
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
62
|
+
"sec-ch-ua-platform-version": '"15.0.0"',
|
|
63
|
+
"sec-ch-ua-arch": '"x86"',
|
|
64
|
+
"sec-ch-ua-bitness": '"64"',
|
|
65
|
+
"sec-ch-ua-full-version": '"138.0.7204.184"',
|
|
66
|
+
"sec-ch-ua-full-version-list": '"Not)A;Brand";v="8.0.0.0", "Chromium";v="138.0.7204.184", "Google Chrome";v="138.0.7204.184"',
|
|
67
|
+
"sec-ch-ua-model": '""',
|
|
68
|
+
"sec-fetch-dest": "empty",
|
|
69
|
+
"sec-fetch-mode": "cors",
|
|
70
|
+
"sec-fetch-site": "same-origin",
|
|
71
|
+
"x-app-apiclient": "default",
|
|
72
|
+
"x-app-apiversion": "2.18",
|
|
73
|
+
"x-perplexity-request-try-number": "1",
|
|
74
|
+
"x-perplexity-request-reason": "use-preferred-search-models",
|
|
75
|
+
accept: "*/*",
|
|
76
|
+
"accept-language": "en-US,en;q=0.9",
|
|
77
|
+
referer: `${PERPLEXITY_URL}/`,
|
|
78
|
+
origin: PERPLEXITY_URL
|
|
79
|
+
};
|
|
80
|
+
function noopLog(_) {
|
|
81
|
+
}
|
|
82
|
+
function buildCookieHeader(cookies) {
|
|
83
|
+
return cookies.filter((c) => c.domain?.includes("perplexity.ai")).map((c) => `${c.name}=${c.value}`).join("; ");
|
|
84
|
+
}
|
|
85
|
+
function detectCfChallenge(body) {
|
|
86
|
+
if (!body) return false;
|
|
87
|
+
const head = body.slice(0, 2e3).toLowerCase();
|
|
88
|
+
return head.includes("just a moment") || head.includes("cf-mitigated") || head.includes("enable javascript and cookies to continue") || head.includes("<html") && head.includes("cloudflare");
|
|
89
|
+
}
|
|
90
|
+
async function tierGotScraping(cookies, log, timeoutMs) {
|
|
91
|
+
const started = Date.now();
|
|
92
|
+
let gotScraping;
|
|
93
|
+
try {
|
|
94
|
+
({ gotScraping } = await import("got-scraping"));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { ok: false, error: `got-scraping missing: ${err.message}`, elapsedMs: Date.now() - started };
|
|
97
|
+
}
|
|
98
|
+
const cookieHeader = buildCookieHeader(cookies);
|
|
99
|
+
const headers = {
|
|
100
|
+
cookie: cookieHeader,
|
|
101
|
+
"user-agent": USER_AGENT,
|
|
102
|
+
...CHROME_CLIENT_HINTS
|
|
103
|
+
};
|
|
104
|
+
const fetchJson = async (url, name) => {
|
|
105
|
+
try {
|
|
106
|
+
const res = await gotScraping({
|
|
107
|
+
url,
|
|
108
|
+
headers,
|
|
109
|
+
timeout: { request: timeoutMs },
|
|
110
|
+
throwHttpErrors: false,
|
|
111
|
+
responseType: "text",
|
|
112
|
+
headerGeneratorOptions: {
|
|
113
|
+
browsers: [{ name: "chrome", minVersion: 130 }],
|
|
114
|
+
operatingSystems: ["windows"]
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const body = typeof res.body === "string" ? res.body : String(res.body ?? "");
|
|
118
|
+
if (res.statusCode !== 200) {
|
|
119
|
+
log(`got-scraping: ${name} status=${res.statusCode}`);
|
|
120
|
+
if (detectCfChallenge(body)) return { __challenged: true };
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const ct = res.headers["content-type"] ?? "";
|
|
124
|
+
if (!String(ct).includes("application/json")) {
|
|
125
|
+
log(`got-scraping: ${name} non-JSON content-type=${ct}`);
|
|
126
|
+
if (detectCfChallenge(body)) return { __challenged: true };
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return JSON.parse(body);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
log(`got-scraping: ${name} threw ${err.message}`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
|
|
136
|
+
fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
|
|
137
|
+
fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
|
|
138
|
+
fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
|
|
139
|
+
fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
|
|
140
|
+
fetchJson(USER_INFO_ENDPOINT, "user/info")
|
|
141
|
+
]);
|
|
142
|
+
const challenged = !!(models && models.__challenged);
|
|
143
|
+
if (challenged) {
|
|
144
|
+
return { ok: false, challenged: true, error: "CF challenge via got-scraping", elapsedMs: Date.now() - started };
|
|
145
|
+
}
|
|
146
|
+
if (!models) {
|
|
147
|
+
return { ok: false, error: "got-scraping: models/config fetch failed", elapsedMs: Date.now() - started };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
payload: {
|
|
152
|
+
models,
|
|
153
|
+
asi: asi && !asi.__challenged ? asi : null,
|
|
154
|
+
rateLimits: rateLimits && !rateLimits.__challenged ? rateLimits : null,
|
|
155
|
+
experiments: experiments && !experiments.__challenged ? experiments : null,
|
|
156
|
+
userInfo: userInfo && !userInfo.__challenged ? userInfo : null
|
|
157
|
+
},
|
|
158
|
+
elapsedMs: Date.now() - started
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function loadImpit() {
|
|
162
|
+
const candidates = [
|
|
163
|
+
join(getImpitRuntimeDirPath(), "node_modules", "impit", "index.wrapper.js"),
|
|
164
|
+
join(getImpitRuntimeDirPath(), "node_modules", "impit", "index.js"),
|
|
165
|
+
join(getImpitRuntimeDirPath(), "node_modules", "impit", "dist", "index.js")
|
|
166
|
+
];
|
|
167
|
+
const failures = [];
|
|
168
|
+
for (const p of candidates) {
|
|
169
|
+
if (!existsSync(p)) continue;
|
|
170
|
+
try {
|
|
171
|
+
const { createRequire } = await import("module");
|
|
172
|
+
const req = createRequire(p);
|
|
173
|
+
return req(p);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
failures.push(`${p}: ${err.message ?? err}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (failures.length) {
|
|
179
|
+
try {
|
|
180
|
+
console.error(`[impit] load failed: ${failures.join(" | ")}`);
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function isImpitAvailable() {
|
|
187
|
+
const marker = join(getImpitRuntimeDirPath(), "node_modules", "impit", "package.json");
|
|
188
|
+
return existsSync(marker);
|
|
189
|
+
}
|
|
190
|
+
async function impitFetchJson(url, init = {}, cookies, timeoutMs = 15e3) {
|
|
191
|
+
const impitMod = await loadImpit();
|
|
192
|
+
if (!impitMod) return null;
|
|
193
|
+
let client;
|
|
194
|
+
try {
|
|
195
|
+
client = new impitMod.Impit({ browser: "chrome", ignoreTlsErrors: false });
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const hasBody = init.body !== void 0 && init.body !== null;
|
|
200
|
+
const headers = {
|
|
201
|
+
cookie: buildCookieHeader(cookies),
|
|
202
|
+
"user-agent": USER_AGENT,
|
|
203
|
+
...CHROME_CLIENT_HINTS,
|
|
204
|
+
accept: "application/json, text/plain, */*",
|
|
205
|
+
...hasBody ? { "content-type": "application/json" } : {},
|
|
206
|
+
...init.headers ?? {}
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
const ctrl = new AbortController();
|
|
210
|
+
const to = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
211
|
+
const res = await client.fetch(url, {
|
|
212
|
+
method: init.method ?? "GET",
|
|
213
|
+
headers,
|
|
214
|
+
body: hasBody ? JSON.stringify(init.body) : void 0,
|
|
215
|
+
signal: ctrl.signal
|
|
216
|
+
});
|
|
217
|
+
clearTimeout(to);
|
|
218
|
+
const text = await res.text();
|
|
219
|
+
if (detectCfChallenge(text)) {
|
|
220
|
+
return { status: res.status, data: null, challenged: true };
|
|
221
|
+
}
|
|
222
|
+
let data = text;
|
|
223
|
+
if (text) {
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(text);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { status: res.status, data };
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function tierImpit(cookies, log, timeoutMs) {
|
|
235
|
+
const started = Date.now();
|
|
236
|
+
const impitMod = await loadImpit();
|
|
237
|
+
if (!impitMod) {
|
|
238
|
+
return { ok: false, error: "impit not installed (optional speed boost)", elapsedMs: Date.now() - started };
|
|
239
|
+
}
|
|
240
|
+
let client;
|
|
241
|
+
try {
|
|
242
|
+
client = new impitMod.Impit({ browser: "chrome", ignoreTlsErrors: false });
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return { ok: false, error: `impit init failed: ${err.message}`, elapsedMs: Date.now() - started };
|
|
245
|
+
}
|
|
246
|
+
const cookieHeader = buildCookieHeader(cookies);
|
|
247
|
+
const headers = {
|
|
248
|
+
cookie: cookieHeader,
|
|
249
|
+
"user-agent": USER_AGENT,
|
|
250
|
+
...CHROME_CLIENT_HINTS
|
|
251
|
+
};
|
|
252
|
+
const fetchJson = async (url, name) => {
|
|
253
|
+
try {
|
|
254
|
+
const ctrl = new AbortController();
|
|
255
|
+
const to = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
256
|
+
const res = await client.fetch(url, { headers, signal: ctrl.signal });
|
|
257
|
+
clearTimeout(to);
|
|
258
|
+
const body = await res.text();
|
|
259
|
+
if (res.status !== 200) {
|
|
260
|
+
log(`impit: ${name} status=${res.status}`);
|
|
261
|
+
if (detectCfChallenge(body)) return { __challenged: true };
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const rhdrs = res.headers;
|
|
265
|
+
const ct = typeof rhdrs?.get === "function" ? rhdrs.get("content-type") ?? "" : rhdrs?.["content-type"] ?? "";
|
|
266
|
+
if (!String(ct).includes("application/json")) {
|
|
267
|
+
log(`impit: ${name} non-JSON content-type=${ct}`);
|
|
268
|
+
if (detectCfChallenge(body)) return { __challenged: true };
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return JSON.parse(body);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
log(`impit: ${name} threw ${err.message}`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
|
|
278
|
+
fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
|
|
279
|
+
fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
|
|
280
|
+
fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
|
|
281
|
+
fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
|
|
282
|
+
fetchJson(USER_INFO_ENDPOINT, "user/info")
|
|
283
|
+
]);
|
|
284
|
+
const challenged = !!(models && models.__challenged);
|
|
285
|
+
if (challenged) {
|
|
286
|
+
return { ok: false, challenged: true, error: "CF challenge via impit", elapsedMs: Date.now() - started };
|
|
287
|
+
}
|
|
288
|
+
if (!models) {
|
|
289
|
+
return { ok: false, error: "impit: models/config fetch failed", elapsedMs: Date.now() - started };
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
payload: {
|
|
294
|
+
models,
|
|
295
|
+
asi: asi && !asi.__challenged ? asi : null,
|
|
296
|
+
rateLimits: rateLimits && !rateLimits.__challenged ? rateLimits : null,
|
|
297
|
+
experiments: experiments && !experiments.__challenged ? experiments : null,
|
|
298
|
+
userInfo: userInfo && !userInfo.__challenged ? userInfo : null
|
|
299
|
+
},
|
|
300
|
+
elapsedMs: Date.now() - started
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function tierBrowser(cookies, log, timeoutMs) {
|
|
304
|
+
const started = Date.now();
|
|
305
|
+
try {
|
|
306
|
+
await resolveBrowserExecutable();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return { ok: false, error: err.message, elapsedMs: Date.now() - started };
|
|
309
|
+
}
|
|
310
|
+
let chromium;
|
|
311
|
+
try {
|
|
312
|
+
chromium = (await import("patchright")).chromium;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { ok: false, error: `patchright missing: ${err.message}`, elapsedMs: Date.now() - started };
|
|
315
|
+
}
|
|
316
|
+
const probe = findBrowser();
|
|
317
|
+
const label = probe ? `${probe.channel}${probe.path.toLowerCase().includes("brave") ? " (brave)" : ""}` : "bundled";
|
|
318
|
+
log(`browser: launching headless ${label}`);
|
|
319
|
+
const browser = await chromium.launch({
|
|
320
|
+
headless: true,
|
|
321
|
+
args: STEALTH_ARGS,
|
|
322
|
+
...probe ? { executablePath: probe.path } : {},
|
|
323
|
+
...probe && ["chrome", "msedge", "chromium"].includes(probe.channel) ? { channel: probe.channel } : {},
|
|
324
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
325
|
+
});
|
|
326
|
+
try {
|
|
327
|
+
const context = await getOrCreateContext(browser, {
|
|
328
|
+
viewport: { width: 1920, height: 1080 },
|
|
329
|
+
userAgent: USER_AGENT
|
|
330
|
+
});
|
|
331
|
+
await context.addCookies(cookies);
|
|
332
|
+
const page = await context.newPage();
|
|
333
|
+
const navResp = await page.goto(PERPLEXITY_URL, { waitUntil: "domcontentloaded", timeout: timeoutMs }).catch((e) => {
|
|
334
|
+
log(`browser: goto failed ${e.message}`);
|
|
335
|
+
return null;
|
|
336
|
+
});
|
|
337
|
+
const status = navResp?.status() ?? 0;
|
|
338
|
+
const title = await page.title().catch(() => "");
|
|
339
|
+
log(`browser: navigation status=${status}, title="${title}"`);
|
|
340
|
+
if (title.includes("Just a moment") || title.toLowerCase().includes("cloudflare")) {
|
|
341
|
+
return { ok: false, challenged: true, error: "CF challenge via browser \u2014 cookies expired", elapsedMs: Date.now() - started };
|
|
342
|
+
}
|
|
343
|
+
const fetchJson = async (url, name) => {
|
|
344
|
+
try {
|
|
345
|
+
const result = await page.evaluate(async (u) => {
|
|
346
|
+
try {
|
|
347
|
+
const r = await fetch(u, { credentials: "include" });
|
|
348
|
+
const ct = r.headers.get("content-type") ?? "";
|
|
349
|
+
if (!r.ok || !ct.includes("application/json")) {
|
|
350
|
+
return { ok: false, status: r.status, contentType: ct };
|
|
351
|
+
}
|
|
352
|
+
return { ok: true, body: await r.json() };
|
|
353
|
+
} catch (e) {
|
|
354
|
+
return { ok: false, error: e?.message ?? String(e) };
|
|
355
|
+
}
|
|
356
|
+
}, url);
|
|
357
|
+
if (!result?.ok) {
|
|
358
|
+
log(`browser: ${name} non-OK status=${result?.status ?? "n/a"} ct=${result?.contentType ?? "n/a"} err=${result?.error ?? "n/a"}`);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return result.body;
|
|
362
|
+
} catch (err) {
|
|
363
|
+
log(`browser: ${name} evaluate threw: ${err.message}`);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
|
|
368
|
+
fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
|
|
369
|
+
fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
|
|
370
|
+
fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
|
|
371
|
+
fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
|
|
372
|
+
fetchJson(USER_INFO_ENDPOINT, "user/info")
|
|
373
|
+
]);
|
|
374
|
+
if (!models) {
|
|
375
|
+
return { ok: false, error: "browser: models/config fetch failed inside page", elapsedMs: Date.now() - started };
|
|
376
|
+
}
|
|
377
|
+
log(
|
|
378
|
+
`browser: fetched \u2014 models=${Object.keys(models.models || {}).length}, asi=${!!asi}, rateLimits=${!!rateLimits}, experiments=${experiments ? Object.keys(experiments).length + " keys" : "null"}, userInfo=${userInfo ? JSON.stringify(userInfo) : "null"}`
|
|
379
|
+
);
|
|
380
|
+
const freshCookies = await context.cookies(PERPLEXITY_URL).then(
|
|
381
|
+
(arr) => arr.map((c) => ({
|
|
382
|
+
name: c.name,
|
|
383
|
+
value: c.value,
|
|
384
|
+
domain: c.domain,
|
|
385
|
+
path: c.path,
|
|
386
|
+
secure: c.secure,
|
|
387
|
+
httpOnly: c.httpOnly,
|
|
388
|
+
sameSite: c.sameSite
|
|
389
|
+
}))
|
|
390
|
+
).catch(() => void 0);
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
payload: { models, asi, rateLimits, experiments, userInfo, freshCookies },
|
|
394
|
+
elapsedMs: Date.now() - started
|
|
395
|
+
};
|
|
396
|
+
} finally {
|
|
397
|
+
await browser.close().catch(() => void 0);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function refreshAccountInfo(opts = {}) {
|
|
401
|
+
const log = opts.log ?? noopLog;
|
|
402
|
+
const started = Date.now();
|
|
403
|
+
const timeoutMs = opts.timeoutMs ?? 25e3;
|
|
404
|
+
const modelsCacheFile = getModelsCacheFile();
|
|
405
|
+
const legacyCookiesFile = getLegacyCookiesFile();
|
|
406
|
+
const savedCookies = await getSavedCookies();
|
|
407
|
+
if (savedCookies.length === 0) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
source: "no-cookies",
|
|
411
|
+
tier: null,
|
|
412
|
+
modelCount: 0,
|
|
413
|
+
accountTier: "Unknown",
|
|
414
|
+
error: "No saved cookies \u2014 run Perplexity: Login first.",
|
|
415
|
+
cachePath: modelsCacheFile,
|
|
416
|
+
elapsedMs: Date.now() - started
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const attempts = [];
|
|
420
|
+
const tierMap = {
|
|
421
|
+
"got-scraping": tierGotScraping,
|
|
422
|
+
impit: tierImpit,
|
|
423
|
+
browser: tierBrowser
|
|
424
|
+
};
|
|
425
|
+
const pipeline = opts.forceTier ? [[opts.forceTier, tierMap[opts.forceTier]]] : (() => {
|
|
426
|
+
const chain = [["got-scraping", tierGotScraping]];
|
|
427
|
+
if (isImpitAvailable()) chain.push(["impit", tierImpit]);
|
|
428
|
+
chain.push(["browser", tierBrowser]);
|
|
429
|
+
return chain;
|
|
430
|
+
})();
|
|
431
|
+
let successful = null;
|
|
432
|
+
let lastChallenged = false;
|
|
433
|
+
for (const [name, fn] of pipeline) {
|
|
434
|
+
log(`refresh: attempting tier=${name}`);
|
|
435
|
+
const result2 = await fn(savedCookies, log, timeoutMs);
|
|
436
|
+
attempts.push({ tier: name, ok: result2.ok, elapsedMs: result2.elapsedMs, error: result2.error });
|
|
437
|
+
if (result2.ok) {
|
|
438
|
+
successful = { tier: name, result: result2 };
|
|
439
|
+
log(`refresh: tier=${name} succeeded in ${result2.elapsedMs}ms`);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
lastChallenged = lastChallenged || !!result2.challenged;
|
|
443
|
+
log(`refresh: tier=${name} failed in ${result2.elapsedMs}ms (${result2.error ?? "unknown"})`);
|
|
444
|
+
}
|
|
445
|
+
if (!successful) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
source: lastChallenged ? "cf-challenge" : "failed",
|
|
449
|
+
tier: null,
|
|
450
|
+
modelCount: 0,
|
|
451
|
+
accountTier: "Unknown",
|
|
452
|
+
error: lastChallenged ? "All tiers hit Cloudflare challenge. Run Perplexity: Login to re-solve Turnstile." : "All refresh tiers failed. See logs.",
|
|
453
|
+
cachePath: modelsCacheFile,
|
|
454
|
+
elapsedMs: Date.now() - started,
|
|
455
|
+
tierAttempts: attempts
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const { tier, result } = successful;
|
|
459
|
+
const payload = result.payload;
|
|
460
|
+
const existing = readExistingCache(modelsCacheFile);
|
|
461
|
+
const isEnterpriseFromUser = payload.userInfo?.is_enterprise === true;
|
|
462
|
+
const isEnterpriseFromExp = payload.experiments?.server_is_enterprise === true;
|
|
463
|
+
const isProFromExp = payload.experiments?.server_is_pro === true;
|
|
464
|
+
const isMaxFromExp = payload.experiments?.server_is_max === true;
|
|
465
|
+
const canUseComputer = payload.asi?.can_use_computer ?? existing?.canUseComputer ?? false;
|
|
466
|
+
const info = {
|
|
467
|
+
// If Computer mode is accessible but no experiments flag fires, infer Pro —
|
|
468
|
+
// Computer is gated to paid tiers, so the account is at least Pro.
|
|
469
|
+
isPro: isProFromExp || canUseComputer && !isMaxFromExp && !isEnterpriseFromUser && !isEnterpriseFromExp,
|
|
470
|
+
isMax: isMaxFromExp,
|
|
471
|
+
isEnterprise: isEnterpriseFromUser || isEnterpriseFromExp,
|
|
472
|
+
canUseComputer,
|
|
473
|
+
modelsConfig: payload.models,
|
|
474
|
+
rateLimits: payload.rateLimits ?? existing?.rateLimits ?? null
|
|
475
|
+
};
|
|
476
|
+
mkdirSync(dirname(modelsCacheFile), { recursive: true });
|
|
477
|
+
writeFileSync(modelsCacheFile, JSON.stringify(info, null, 2));
|
|
478
|
+
log(`refresh: wrote ${modelsCacheFile} (${Object.keys(payload.models.models || {}).length} models)`);
|
|
479
|
+
if (payload.freshCookies && payload.freshCookies.length > 0) {
|
|
480
|
+
try {
|
|
481
|
+
mkdirSync(dirname(legacyCookiesFile), { recursive: true });
|
|
482
|
+
writeFileSync(
|
|
483
|
+
legacyCookiesFile,
|
|
484
|
+
JSON.stringify({ allCookies: payload.freshCookies, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
485
|
+
);
|
|
486
|
+
log(`refresh: persisted ${payload.freshCookies.length} fresh cookies to ${legacyCookiesFile}`);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
log(`refresh: could not persist fresh cookies (non-fatal): ${err.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
ok: true,
|
|
493
|
+
source: "live",
|
|
494
|
+
tier,
|
|
495
|
+
modelCount: Object.keys(payload.models.models || {}).length,
|
|
496
|
+
accountTier: deriveTier(info),
|
|
497
|
+
cachePath: modelsCacheFile,
|
|
498
|
+
elapsedMs: Date.now() - started,
|
|
499
|
+
tierAttempts: attempts
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function deriveTier(info) {
|
|
503
|
+
if (info.isMax) return "Max";
|
|
504
|
+
if (info.isEnterprise) return "Enterprise";
|
|
505
|
+
if (info.isPro) return "Pro";
|
|
506
|
+
return "Free";
|
|
507
|
+
}
|
|
508
|
+
function readExistingCache(modelsCacheFile = getModelsCacheFile()) {
|
|
509
|
+
if (!existsSync(modelsCacheFile)) return null;
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(readFileSync(modelsCacheFile, "utf8"));
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function getModelsCacheInfo() {
|
|
517
|
+
const path = getModelsCacheFile();
|
|
518
|
+
const exists = existsSync(path);
|
|
519
|
+
const mtime = exists ? statSync(path).mtime : null;
|
|
520
|
+
const ageHours = mtime ? (Date.now() - mtime.getTime()) / 36e5 : null;
|
|
521
|
+
return { path, exists, mtime, ageHours };
|
|
522
|
+
}
|
|
523
|
+
function getImpitRuntimeDir() {
|
|
524
|
+
return getImpitRuntimeDirPath();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export {
|
|
528
|
+
loadImpit,
|
|
529
|
+
isImpitAvailable,
|
|
530
|
+
impitFetchJson,
|
|
531
|
+
refreshAccountInfo,
|
|
532
|
+
getModelsCacheInfo,
|
|
533
|
+
getImpitRuntimeDir
|
|
534
|
+
};
|