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.
Files changed (125) hide show
  1. package/README.md +192 -0
  2. package/dist/attachments.d.ts +20 -0
  3. package/dist/attachments.mjs +43 -0
  4. package/dist/checks/browser.d.ts +100 -0
  5. package/dist/checks/browser.mjs +89 -0
  6. package/dist/checks/config.d.ts +91 -0
  7. package/dist/checks/config.mjs +88 -0
  8. package/dist/checks/ide.d.ts +89 -0
  9. package/dist/checks/ide.mjs +80 -0
  10. package/dist/checks/mcp.d.ts +61 -0
  11. package/dist/checks/mcp.mjs +56 -0
  12. package/dist/checks/native-deps.d.ts +131 -0
  13. package/dist/checks/native-deps.mjs +115 -0
  14. package/dist/checks/network.d.ts +71 -0
  15. package/dist/checks/network.mjs +70 -0
  16. package/dist/checks/probe.d.ts +93 -0
  17. package/dist/checks/probe.mjs +82 -0
  18. package/dist/checks/profiles.d.ts +99 -0
  19. package/dist/checks/profiles.mjs +90 -0
  20. package/dist/checks/runtime.d.ts +89 -0
  21. package/dist/checks/runtime.mjs +90 -0
  22. package/dist/checks/vault.d.ts +101 -0
  23. package/dist/checks/vault.mjs +90 -0
  24. package/dist/chunk-3B276PGG.mjs +115 -0
  25. package/dist/chunk-4UEJOM6W.mjs +9 -0
  26. package/dist/chunk-6EP2BLTV.mjs +205 -0
  27. package/dist/chunk-6YMQVLFX.mjs +146 -0
  28. package/dist/chunk-7JL36EBH.mjs +118 -0
  29. package/dist/chunk-DPGMKSSA.mjs +57 -0
  30. package/dist/chunk-H4BUAPPO.mjs +1950 -0
  31. package/dist/chunk-HMKLWVXB.mjs +109 -0
  32. package/dist/chunk-HTUAQRKH.mjs +125 -0
  33. package/dist/chunk-HU5B4FXS.mjs +139 -0
  34. package/dist/chunk-KCXM2M4B.mjs +1006 -0
  35. package/dist/chunk-LKJMLGFP.mjs +237 -0
  36. package/dist/chunk-LZPLNZ5U.mjs +67 -0
  37. package/dist/chunk-MTDFKNXX.mjs +19 -0
  38. package/dist/chunk-OF4DMAPJ.mjs +511 -0
  39. package/dist/chunk-PE23RMXY.mjs +43 -0
  40. package/dist/chunk-Q2VY4R5F.mjs +175 -0
  41. package/dist/chunk-S5VD7WTU.mjs +2540 -0
  42. package/dist/chunk-SVPRB62V.mjs +106 -0
  43. package/dist/chunk-TQLCLE4L.mjs +345 -0
  44. package/dist/chunk-U3DGFLXZ.mjs +43 -0
  45. package/dist/chunk-X45O6YD3.mjs +688 -0
  46. package/dist/chunk-XKSWCEGI.mjs +168 -0
  47. package/dist/chunk-Z7DAACGZ.mjs +534 -0
  48. package/dist/chunk-ZQFUZPLO.mjs +257 -0
  49. package/dist/cli.d.ts +952 -0
  50. package/dist/cli.mjs +827 -0
  51. package/dist/client.d.ts +355 -0
  52. package/dist/client.mjs +27 -0
  53. package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
  54. package/dist/cloud-sync.d.ts +42 -0
  55. package/dist/cloud-sync.mjs +17 -0
  56. package/dist/config.d.ts +186 -0
  57. package/dist/config.mjs +54 -0
  58. package/dist/daemon/attach.d.ts +36 -0
  59. package/dist/daemon/attach.mjs +25 -0
  60. package/dist/daemon/audit.d.ts +23 -0
  61. package/dist/daemon/audit.mjs +12 -0
  62. package/dist/daemon/client-http.d.ts +42 -0
  63. package/dist/daemon/client-http.mjs +29 -0
  64. package/dist/daemon/index.d.ts +14 -0
  65. package/dist/daemon/index.mjs +110 -0
  66. package/dist/daemon/install-tunnel.d.ts +46 -0
  67. package/dist/daemon/install-tunnel.mjs +14 -0
  68. package/dist/daemon/launcher.d.ts +163 -0
  69. package/dist/daemon/launcher.mjs +50 -0
  70. package/dist/daemon/lockfile.d.ts +29 -0
  71. package/dist/daemon/lockfile.mjs +18 -0
  72. package/dist/daemon/server.d.ts +159 -0
  73. package/dist/daemon/server.mjs +20 -0
  74. package/dist/daemon/token.d.ts +17 -0
  75. package/dist/daemon/token.mjs +17 -0
  76. package/dist/daemon/tunnel-providers/index.d.ts +330 -0
  77. package/dist/daemon/tunnel-providers/index.mjs +57 -0
  78. package/dist/daemon/tunnel.d.ts +23 -0
  79. package/dist/daemon/tunnel.mjs +9 -0
  80. package/dist/doctor-report.d.ts +24 -0
  81. package/dist/doctor-report.mjs +14 -0
  82. package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
  83. package/dist/doctor.d.ts +44 -0
  84. package/dist/doctor.mjs +16 -0
  85. package/dist/export.d.ts +19 -0
  86. package/dist/export.mjs +15 -0
  87. package/dist/health-check.d.ts +108 -0
  88. package/dist/health-check.mjs +92 -0
  89. package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
  90. package/dist/history-store.d.ts +65 -0
  91. package/dist/history-store.mjs +48 -0
  92. package/dist/impit-login-runner.d.ts +469 -0
  93. package/dist/impit-login-runner.mjs +685 -0
  94. package/dist/index.d.ts +159 -0
  95. package/dist/index.mjs +236 -0
  96. package/dist/login-runner.d.ts +333 -0
  97. package/dist/login-runner.mjs +320 -0
  98. package/dist/logout.d.ts +28 -0
  99. package/dist/logout.mjs +45 -0
  100. package/dist/manual-login-runner.d.ts +150 -0
  101. package/dist/manual-login-runner.mjs +146 -0
  102. package/dist/native-deps-BNThFHxa.d.ts +175 -0
  103. package/dist/native-deps-YNKXITRY.mjs +139 -0
  104. package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
  105. package/dist/profiles.d.ts +41 -0
  106. package/dist/profiles.mjs +33 -0
  107. package/dist/redact.d.ts +159 -0
  108. package/dist/redact.mjs +11 -0
  109. package/dist/refresh.d.ts +118 -0
  110. package/dist/refresh.mjs +21 -0
  111. package/dist/reinit-watcher.d.ts +15 -0
  112. package/dist/reinit-watcher.mjs +8 -0
  113. package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
  114. package/dist/tty-prompt.d.ts +44 -0
  115. package/dist/tty-prompt.mjs +39 -0
  116. package/dist/vault.d-BtRSLZiM.d.ts +8 -0
  117. package/dist/vault.d.ts +37 -0
  118. package/dist/vault.mjs +21 -0
  119. package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
  120. package/dist/viewer-detect.d.ts +4 -0
  121. package/dist/viewer-detect.mjs +37 -0
  122. package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
  123. package/dist/viewers.d.ts +18 -0
  124. package/dist/viewers.mjs +122 -0
  125. package/package.json +152 -0
@@ -0,0 +1,1950 @@
1
+ import {
2
+ impitFetchJson,
3
+ isImpitAvailable
4
+ } from "./chunk-Z7DAACGZ.mjs";
5
+ import {
6
+ FORMAT_TO_CONTENT_TYPE,
7
+ exportThread,
8
+ resolveExportApiFormat
9
+ } from "./chunk-LZPLNZ5U.mjs";
10
+ import {
11
+ ASI_ACCESS_ENDPOINT,
12
+ AUTH_SESSION_ENDPOINT,
13
+ EXPERIMENTS_ENDPOINT,
14
+ MODELS_CONFIG_ENDPOINT,
15
+ PERPLEXITY_URL,
16
+ QUERY_ENDPOINT,
17
+ RATE_LIMIT_ENDPOINT,
18
+ SUPPORTED_BLOCK_USE_CASES,
19
+ THREAD_ENDPOINT,
20
+ findBrowser,
21
+ getOrCreateContext,
22
+ getSavedCookies,
23
+ resolveBrowserExecutable
24
+ } from "./chunk-LKJMLGFP.mjs";
25
+ import {
26
+ getActiveName,
27
+ getConfigDir,
28
+ getProfilePaths
29
+ } from "./chunk-XKSWCEGI.mjs";
30
+
31
+ // src/client.ts
32
+ import { randomUUID } from "crypto";
33
+ import { chromium } from "patchright";
34
+ import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
35
+ import { join as join2 } from "path";
36
+
37
+ // src/fs-utils.js
38
+ import { unlinkSync } from "fs";
39
+ import { join } from "path";
40
+ var SINGLETON_FILES = ["SingletonLock", "SingletonCookie", "SingletonSocket"];
41
+ function clearStaleSingletonLocks(dir) {
42
+ for (const name of SINGLETON_FILES) {
43
+ try {
44
+ unlinkSync(join(dir, name));
45
+ } catch (err) {
46
+ if (err && err.code !== "ENOENT") {
47
+ console.error(`[perplexity-mcp] Could not remove ${name} in ${dir}:`, err.message);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ // src/client.ts
54
+ function getActiveProfileName() {
55
+ return process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
56
+ }
57
+ function getActivePaths() {
58
+ return getProfilePaths(getActiveProfileName());
59
+ }
60
+ function getModelsCacheFile() {
61
+ return getActivePaths().modelsCache;
62
+ }
63
+ function readCachedAccountInfoFromDisk() {
64
+ const modelsCacheFile = getModelsCacheFile();
65
+ if (!existsSync(modelsCacheFile)) return null;
66
+ try {
67
+ const cached = JSON.parse(readFileSync(modelsCacheFile, "utf-8"));
68
+ return cached;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ var STEALTH_ARGS = [
74
+ "--disable-blink-features=AutomationControlled",
75
+ // NOTE: `--disable-web-security` was removed (2026-04-27 public-hardening
76
+ // audit). All in-page `fetch()` calls in this file are same-origin
77
+ // (perplexity.ai) — the only off-origin downloader (`downloadASIFiles`)
78
+ // now uses Playwright's `APIRequestContext` (`context.request.get`) which
79
+ // runs outside the page context and is not subject to CORS. Re-adding this
80
+ // flag would re-introduce a meaningful XSS amplification risk for no gain.
81
+ //
82
+ // NOTE: `--disable-features=IsolateOrigins,site-per-process` and
83
+ // `--disable-site-isolation-trials` were removed (2026-04-27 public-
84
+ // hardening audit). They disable Chromium's Site Isolation process model,
85
+ // which is a renderer-architecture feature invisible to JavaScript on the
86
+ // page (no documented fingerprint surface — Patchright's
87
+ // `chromiumSwitches.js` does not include them; see
88
+ // node_modules/patchright-core/lib/server/chromium/chromiumSwitches.js).
89
+ // Their historical use in puppeteer-stealth recipes was to keep cross-
90
+ // origin iframes in the same renderer process so `page.frames()` /
91
+ // CDP-based interaction worked uniformly. This codebase does not touch
92
+ // iframes (no `page.frames`, `frameLocator`, `mainFrame`, or `postMessage`
93
+ // usage in packages/mcp-server/src), so the only effect of keeping them
94
+ // was a silent reduction in the browser's Spectre/UXSS defense-in-depth.
95
+ "--no-first-run",
96
+ "--no-default-browser-check",
97
+ "--disable-infobars",
98
+ "--disable-extensions",
99
+ "--disable-popup-blocking"
100
+ ];
101
+ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
102
+ function buildLaunchOptions(headless) {
103
+ const browser = findBrowser();
104
+ const opts = {
105
+ headless,
106
+ args: STEALTH_ARGS,
107
+ viewport: headless ? { width: 1920, height: 1080 } : { width: 800, height: 600 },
108
+ userAgent: USER_AGENT,
109
+ // Strip --enable-automation (Playwright default) which is a CF red flag
110
+ ignoreDefaultArgs: ["--enable-automation"]
111
+ };
112
+ if (browser) {
113
+ opts.executablePath = browser.path;
114
+ if (browser.channel === "chrome" || browser.channel === "msedge" || browser.channel === "chromium") {
115
+ opts.channel = browser.channel;
116
+ }
117
+ console.error(`[perplexity-mcp] Using ${browser.channel}: ${browser.path}`);
118
+ }
119
+ return opts;
120
+ }
121
+ function deriveTierFlagsFromExperiments(experiments, canUseComputer) {
122
+ const isProFromExp = experiments?.server_is_pro === true;
123
+ const isMax = experiments?.server_is_max === true;
124
+ const isEnterprise = experiments?.server_is_enterprise === true;
125
+ const isPro = isProFromExp || canUseComputer && !isMax && !isEnterprise;
126
+ return { isPro, isMax, isEnterprise };
127
+ }
128
+ function buildListAskThreadsUrl() {
129
+ return `${PERPLEXITY_URL}/rest/thread/list_ask_threads?version=2.18&source=default`;
130
+ }
131
+ function buildListAskThreadsBody(opts) {
132
+ return {
133
+ limit: opts.limit ?? 1e3,
134
+ offset: opts.offset ?? 0,
135
+ ascending: opts.ascending ?? false,
136
+ search_term: opts.searchTerm ?? "",
137
+ with_temporary_threads: false,
138
+ exclude_asi: opts.excludeAsi ?? false
139
+ };
140
+ }
141
+ function parseListThreadsRows(rows) {
142
+ const total = typeof rows[0]?.total_threads === "number" ? rows[0].total_threads : rows.length;
143
+ return {
144
+ total,
145
+ items: rows.map((row) => ({
146
+ backendUuid: String(row.uuid ?? ""),
147
+ contextUuid: String(row.context_uuid ?? ""),
148
+ slug: String(row.slug ?? ""),
149
+ title: String(row.title ?? row.query_str ?? "(untitled)"),
150
+ queryStr: String(row.query_str ?? ""),
151
+ answerPreview: String(row.answer_preview ?? "").slice(0, 220),
152
+ firstAnswer: typeof row.first_answer === "string" ? row.first_answer : null,
153
+ createdAt: typeof row.last_query_datetime === "string" ? /[Zz]$/.test(row.last_query_datetime) ? row.last_query_datetime : `${row.last_query_datetime}Z` : (/* @__PURE__ */ new Date()).toISOString(),
154
+ mode: typeof row.mode === "string" ? row.mode : null,
155
+ displayModel: typeof row.display_model === "string" ? row.display_model : null,
156
+ searchFocus: typeof row.search_focus === "string" ? row.search_focus : null,
157
+ sources: Array.isArray(row.sources) ? row.sources.map(String) : [],
158
+ queryCount: typeof row.query_count === "number" ? row.query_count : 1,
159
+ threadStatus: String(row.thread_status ?? row.status ?? "completed").toLowerCase(),
160
+ readWriteToken: typeof row.read_write_token === "string" ? row.read_write_token : null
161
+ }))
162
+ };
163
+ }
164
+ async function listCloudThreadsViaImpit(opts = {}) {
165
+ if (!isImpitAvailable()) return null;
166
+ const cookies = await getSavedCookies().catch(() => []);
167
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
168
+ if (!hasSession) return null;
169
+ const url = buildListAskThreadsUrl();
170
+ const body = buildListAskThreadsBody(opts);
171
+ const headers = {
172
+ "x-app-apiclient": "default",
173
+ "x-app-apiversion": "2.18",
174
+ "x-perplexity-request-endpoint": url,
175
+ "x-perplexity-request-reason": "threads-body",
176
+ "x-perplexity-request-try-number": "1",
177
+ "sec-fetch-dest": "empty",
178
+ "sec-fetch-mode": "cors",
179
+ "sec-fetch-site": "same-origin",
180
+ referer: `${PERPLEXITY_URL}/`,
181
+ origin: PERPLEXITY_URL
182
+ };
183
+ const result = await impitFetchJson(url, { method: "POST", body, headers }, cookies, 6e4);
184
+ if (!result || result.challenged || result.status !== 200 || !Array.isArray(result.data)) {
185
+ console.error(
186
+ `[perplexity-mcp] list_ask_threads impit miss (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); caller will fall back to browser.`
187
+ );
188
+ return null;
189
+ }
190
+ const parsed = parseListThreadsRows(result.data);
191
+ console.error(
192
+ `[perplexity-mcp] list_ask_threads via impit: ${parsed.items.length} rows (offset=${opts.offset ?? 0} limit=${opts.limit ?? 1e3} total=${parsed.total})`
193
+ );
194
+ return parsed;
195
+ }
196
+ function buildGetCloudThreadUrl(slug, limit) {
197
+ return `${PERPLEXITY_URL}/rest/thread/${encodeURIComponent(slug)}?version=2.18&source=default&limit=${limit}&offset=0&from_first=true&with_parent_info=true`;
198
+ }
199
+ function getCloudThreadHeaders(url) {
200
+ return {
201
+ "x-app-apiclient": "default",
202
+ "x-app-apiversion": "2.18",
203
+ "x-perplexity-request-endpoint": url,
204
+ "x-perplexity-request-reason": "thread-body",
205
+ "x-perplexity-request-try-number": "1",
206
+ "sec-fetch-dest": "empty",
207
+ "sec-fetch-mode": "cors",
208
+ "sec-fetch-site": "same-origin",
209
+ referer: `${PERPLEXITY_URL}/`,
210
+ origin: PERPLEXITY_URL
211
+ };
212
+ }
213
+ function parseAnswerText(text) {
214
+ if (typeof text !== "string") return { answer: "", sources: [] };
215
+ let steps;
216
+ try {
217
+ steps = JSON.parse(text);
218
+ } catch {
219
+ return { answer: "", sources: [] };
220
+ }
221
+ if (!Array.isArray(steps)) return { answer: "", sources: [] };
222
+ const final = steps.find((s) => s?.step_type === "FINAL");
223
+ const answerRaw = final?.content?.answer;
224
+ if (typeof answerRaw !== "string") return { answer: "", sources: [] };
225
+ let parsed = {};
226
+ try {
227
+ parsed = JSON.parse(answerRaw);
228
+ } catch {
229
+ return { answer: answerRaw, sources: [] };
230
+ }
231
+ const answer = typeof parsed.answer === "string" ? parsed.answer : "";
232
+ const webResults = Array.isArray(parsed.web_results) ? parsed.web_results : [];
233
+ const sources = webResults.map((wr, i) => ({
234
+ n: i + 1,
235
+ title: String(wr.name ?? ""),
236
+ url: String(wr.url ?? ""),
237
+ ...typeof wr.snippet === "string" && wr.snippet ? { snippet: wr.snippet } : {}
238
+ })).filter((s) => s.title || s.url);
239
+ return { answer, sources };
240
+ }
241
+ function parseCloudThreadResponse(slug, body) {
242
+ const rawEntries = Array.isArray(body.entries) ? body.entries : [];
243
+ return {
244
+ thread: {
245
+ slug,
246
+ title: typeof rawEntries[0]?.thread_title === "string" ? rawEntries[0].thread_title : null,
247
+ contextUuid: typeof rawEntries[0]?.context_uuid === "string" ? rawEntries[0].context_uuid : null
248
+ },
249
+ entries: rawEntries.map((e) => {
250
+ const { answer, sources: s } = parseAnswerText(e.text);
251
+ const srcFromBlock = Array.isArray(e.sources) ? e.sources.map((wr, i) => ({
252
+ n: i + 1,
253
+ title: String(wr.name ?? wr.title ?? ""),
254
+ url: String(wr.url ?? ""),
255
+ ...typeof wr.snippet === "string" ? { snippet: wr.snippet } : {}
256
+ })).filter((src) => src.title || src.url) : [];
257
+ const createdUs = typeof e.created_us === "number" ? e.created_us : 0;
258
+ const iso = typeof e.updated_datetime === "string" ? /[Zz]$/.test(e.updated_datetime) ? e.updated_datetime : `${e.updated_datetime}Z` : createdUs > 0 ? new Date(Math.floor(createdUs / 1e3)).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
259
+ return {
260
+ backendUuid: String(e.backend_uuid ?? ""),
261
+ queryStr: String(e.query_str ?? ""),
262
+ answer: answer || "",
263
+ sources: s.length ? s : srcFromBlock,
264
+ mediaItems: Array.isArray(e.media_items) ? e.media_items.map((m) => ({
265
+ url: String(m.url ?? m.image ?? ""),
266
+ name: typeof m.name === "string" ? m.name : void 0,
267
+ type: typeof m.type === "string" ? m.type : void 0
268
+ })).filter((m) => m.url) : [],
269
+ createdAt: iso,
270
+ status: String(e.status ?? "completed").toLowerCase()
271
+ };
272
+ })
273
+ };
274
+ }
275
+ async function getCloudThreadViaImpit(slug, opts = {}) {
276
+ if (!slug) return null;
277
+ if (!isImpitAvailable()) return null;
278
+ const cookies = await getSavedCookies().catch(() => []);
279
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
280
+ if (!hasSession) return null;
281
+ const limit = opts.limit ?? 50;
282
+ const url = buildGetCloudThreadUrl(slug, limit);
283
+ const result = await impitFetchJson(url, { method: "GET", headers: getCloudThreadHeaders(url) }, cookies, 6e4);
284
+ if (!result || result.challenged || result.status !== 200 || typeof result.data !== "object" || result.data == null) {
285
+ console.error(
286
+ `[perplexity-mcp] get_cloud_thread impit miss slug=${slug} (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); caller will fall back to browser.`
287
+ );
288
+ return null;
289
+ }
290
+ const parsed = parseCloudThreadResponse(slug, result.data);
291
+ console.error(
292
+ `[perplexity-mcp] get_cloud_thread via impit: slug=${slug} entries=${parsed.entries.length} (limit=${limit})`
293
+ );
294
+ return parsed;
295
+ }
296
+ function buildSearchRequest(opts) {
297
+ const frontendUuid = randomUUID();
298
+ const frontendContextUuid = randomUUID();
299
+ const requestId = randomUUID();
300
+ const isFollowup = !!opts.followUp?.backendUuid;
301
+ const params = {
302
+ attachments: [],
303
+ language: opts.language,
304
+ timezone: "America/Los_Angeles",
305
+ search_focus: "internet",
306
+ sources: opts.sources,
307
+ search_recency_filter: null,
308
+ frontend_uuid: frontendUuid,
309
+ mode: opts.mode,
310
+ model_preference: opts.modelPreference,
311
+ is_related_query: false,
312
+ is_sponsored: false,
313
+ frontend_context_uuid: frontendContextUuid,
314
+ prompt_source: "user",
315
+ query_source: isFollowup ? "followup" : "home",
316
+ is_incognito: false,
317
+ time_from_first_type: 5e3 + Math.floor(Math.random() * 15e3),
318
+ local_search_enabled: false,
319
+ use_schematized_api: true,
320
+ send_back_text_in_streaming_api: false,
321
+ supported_block_use_cases: SUPPORTED_BLOCK_USE_CASES,
322
+ client_coordinates: null,
323
+ mentions: [],
324
+ dsl_query: opts.query,
325
+ skip_search_enabled: true,
326
+ is_nav_suggestions_disabled: false,
327
+ source: "default",
328
+ always_search_override: false,
329
+ override_no_search: false,
330
+ should_ask_for_mcp_tool_confirmation: true,
331
+ browser_agent_allow_once_from_toggle: false,
332
+ force_enable_browser_agent: false,
333
+ supported_features: ["browser_agent_permission_banner_v1.1"],
334
+ version: "2.18"
335
+ };
336
+ if (isFollowup) {
337
+ params.last_backend_uuid = opts.followUp.backendUuid;
338
+ params.read_write_token = opts.followUp.readWriteToken ?? null;
339
+ params.followup_source = "link";
340
+ }
341
+ return { body: { params, query_str: opts.query }, requestId };
342
+ }
343
+ function isExperimentalImpitSearchEnabled() {
344
+ return process.env.PERPLEXITY_EXPERIMENTAL_IMPIT_SEARCH === "1";
345
+ }
346
+ async function searchPerplexityViaImpit(opts) {
347
+ if (!isExperimentalImpitSearchEnabled()) return null;
348
+ if (!isImpitAvailable()) return null;
349
+ const cookies = await getSavedCookies().catch(() => []);
350
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
351
+ if (!hasSession) return null;
352
+ const { body, requestId } = buildSearchRequest(opts);
353
+ const headers = {
354
+ accept: "text/event-stream",
355
+ "x-perplexity-request-reason": "perplexity-query-state-provider",
356
+ "x-request-id": requestId,
357
+ "x-app-apiclient": "default",
358
+ "x-app-apiversion": "2.18",
359
+ "x-perplexity-request-endpoint": QUERY_ENDPOINT,
360
+ "x-perplexity-request-try-number": "1",
361
+ "sec-fetch-dest": "empty",
362
+ "sec-fetch-mode": "cors",
363
+ "sec-fetch-site": "same-origin",
364
+ referer: `${PERPLEXITY_URL}/`,
365
+ origin: PERPLEXITY_URL
366
+ };
367
+ const result = await impitFetchJson(QUERY_ENDPOINT, { method: "POST", body, headers }, cookies, 18e4);
368
+ if (!result || result.challenged || result.status !== 200) {
369
+ console.error(
370
+ `[perplexity-mcp] search impit miss (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); caller will fall back to browser.`
371
+ );
372
+ return null;
373
+ }
374
+ if (typeof result.data !== "string" || result.data.length === 0) {
375
+ console.error(`[perplexity-mcp] search impit returned non-text response (typeof=${typeof result.data}); falling back.`);
376
+ return null;
377
+ }
378
+ try {
379
+ const parsed = PerplexityClient.parseSSEText(result.data);
380
+ console.error(
381
+ `[perplexity-mcp] search via impit: model=${opts.modelPreference} answerLen=${parsed.answer?.length ?? 0} sources=${parsed.sources?.length ?? 0}`
382
+ );
383
+ return parsed;
384
+ } catch (err) {
385
+ console.error(`[perplexity-mcp] search impit parse error: ${err.message}; falling back.`);
386
+ return null;
387
+ }
388
+ }
389
+ async function retrieveThreadViaImpit(opts) {
390
+ if (!isImpitAvailable()) return null;
391
+ const cookies = await getSavedCookies().catch(() => []);
392
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
393
+ if (!hasSession) return null;
394
+ const { threadSlug, backendUuid, readWriteToken } = opts;
395
+ const sseHeaders = {
396
+ accept: "text/event-stream",
397
+ "x-perplexity-request-reason": "reconnect-stream",
398
+ "x-app-apiclient": "default",
399
+ "x-app-apiversion": "2.18",
400
+ "x-perplexity-request-try-number": "1",
401
+ "sec-fetch-dest": "empty",
402
+ "sec-fetch-mode": "cors",
403
+ "sec-fetch-site": "same-origin",
404
+ referer: `${PERPLEXITY_URL}/`,
405
+ origin: PERPLEXITY_URL
406
+ };
407
+ if (backendUuid) {
408
+ const url = `${QUERY_ENDPOINT}/reconnect/${backendUuid}`;
409
+ const result = await impitFetchJson(
410
+ url,
411
+ { method: "POST", body: { reconnectInitialSnapshot: true }, headers: { ...sseHeaders, "x-perplexity-request-endpoint": url } },
412
+ cookies,
413
+ 6e4
414
+ );
415
+ if (result && !result.challenged && result.status === 200 && typeof result.data === "string" && result.data.length > 0) {
416
+ try {
417
+ const parsed = PerplexityClient.parseASIReconnectSSE(result.data, threadSlug, backendUuid, readWriteToken ?? "");
418
+ if (!parsed.answer.startsWith("ASI task may still be running")) {
419
+ console.error(
420
+ `[perplexity-mcp] retrieve via impit (reconnect): backendUuid=${backendUuid} answerLen=${parsed.answer?.length ?? 0} files=${parsed.files?.length ?? 0}`
421
+ );
422
+ return parsed;
423
+ }
424
+ const wb = PerplexityClient.extractFromWorkflowBlock(result.data, threadSlug, backendUuid, readWriteToken ?? "");
425
+ if (wb) {
426
+ console.error(
427
+ `[perplexity-mcp] retrieve via impit (workflow-block): backendUuid=${backendUuid} answerLen=${wb.answer?.length ?? 0}`
428
+ );
429
+ return wb;
430
+ }
431
+ } catch (err) {
432
+ console.error(`[perplexity-mcp] retrieve impit reconnect parse error: ${err.message}`);
433
+ }
434
+ } else {
435
+ console.error(
436
+ `[perplexity-mcp] retrieve impit reconnect miss (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); trying thread fallback.`
437
+ );
438
+ }
439
+ }
440
+ if (threadSlug) {
441
+ const url = `${PERPLEXITY_URL}/rest/thread/${encodeURIComponent(threadSlug)}`;
442
+ const result = await impitFetchJson(
443
+ url,
444
+ { method: "GET", headers: { ...sseHeaders, accept: "application/json", "x-perplexity-request-reason": "thread-body", "x-perplexity-request-endpoint": url } },
445
+ cookies,
446
+ 6e4
447
+ );
448
+ if (result && !result.challenged && result.status === 200 && typeof result.data === "object" && result.data !== null) {
449
+ try {
450
+ const threadData = result.data;
451
+ if (threadData.status === "success") {
452
+ const entries = threadData.entries ?? [];
453
+ const lastEntry = entries[entries.length - 1];
454
+ if (lastEntry) {
455
+ const parsed = PerplexityClient.parseASIThreadEntry(lastEntry, threadSlug, backendUuid ?? "", readWriteToken ?? "");
456
+ console.error(
457
+ `[perplexity-mcp] retrieve via impit (thread): slug=${threadSlug} answerLen=${parsed.answer?.length ?? 0}`
458
+ );
459
+ return parsed;
460
+ }
461
+ }
462
+ } catch (err) {
463
+ console.error(`[perplexity-mcp] retrieve impit thread parse error: ${err.message}`);
464
+ }
465
+ } else {
466
+ console.error(
467
+ `[perplexity-mcp] retrieve impit thread miss (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); falling back to browser.`
468
+ );
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+ async function exportThreadViaImpit(opts) {
474
+ if (opts.format === "markdown") return null;
475
+ if (!isImpitAvailable()) return null;
476
+ const cookies = await getSavedCookies().catch(() => []);
477
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
478
+ if (!hasSession) return null;
479
+ let entryUuid = opts.entryUuid ?? null;
480
+ if (!entryUuid && opts.threadSlug) {
481
+ const thread = await getCloudThreadViaImpit(opts.threadSlug, { limit: 1 });
482
+ const entries = thread?.entries ?? [];
483
+ entryUuid = entries[entries.length - 1]?.backendUuid ?? null;
484
+ }
485
+ if (!entryUuid) {
486
+ console.error(
487
+ `[perplexity-mcp] export impit miss (no entry UUID resolvable for slug=${opts.threadSlug ?? "n/a"}); caller will fall back to browser.`
488
+ );
489
+ return null;
490
+ }
491
+ let apiFormat;
492
+ try {
493
+ apiFormat = resolveExportApiFormat(opts.format);
494
+ } catch (err) {
495
+ console.error(`[perplexity-mcp] export impit miss: ${err.message}; falling back.`);
496
+ return null;
497
+ }
498
+ const url = `${PERPLEXITY_URL}/rest/entry/export?version=2.18&source=default`;
499
+ const headers = {
500
+ accept: "application/json",
501
+ "x-app-apiclient": "default",
502
+ "x-app-apiversion": "2.18",
503
+ "x-perplexity-request-endpoint": url,
504
+ "x-perplexity-request-reason": "entry-export",
505
+ "x-perplexity-request-try-number": "1",
506
+ "sec-fetch-dest": "empty",
507
+ "sec-fetch-mode": "cors",
508
+ "sec-fetch-site": "same-origin",
509
+ referer: `${PERPLEXITY_URL}/`,
510
+ origin: PERPLEXITY_URL
511
+ };
512
+ const body = { entry_uuid: entryUuid, format: apiFormat };
513
+ const result = await impitFetchJson(url, { method: "POST", body, headers }, cookies, 6e4);
514
+ if (!result || result.challenged || result.status !== 200 || typeof result.data !== "object" || result.data == null) {
515
+ console.error(
516
+ `[perplexity-mcp] export impit miss (status=${result?.status ?? "n/a"} challenged=${!!result?.challenged}); caller will fall back to browser.`
517
+ );
518
+ return null;
519
+ }
520
+ const data = result.data;
521
+ const buffer = Buffer.from(String(data.file_content_64 ?? ""), "base64");
522
+ if (buffer.length === 0) {
523
+ console.error(`[perplexity-mcp] export impit miss (empty file_content_64); caller will fall back to browser.`);
524
+ return null;
525
+ }
526
+ const filename = String(data.filename ?? `${entryUuid}.${apiFormat}`);
527
+ const contentType = FORMAT_TO_CONTENT_TYPE[opts.format] ?? "application/octet-stream";
528
+ console.error(
529
+ `[perplexity-mcp] export via impit: format=${opts.format} bytes=${buffer.length} filename=${filename}`
530
+ );
531
+ return { buffer, filename, contentType };
532
+ }
533
+ var PerplexityClient = class _PerplexityClient {
534
+ browser = null;
535
+ context = null;
536
+ page = null;
537
+ authenticated = false;
538
+ userId = null;
539
+ accountInfo = {
540
+ isPro: false,
541
+ isMax: false,
542
+ isEnterprise: false,
543
+ canUseComputer: false,
544
+ modelsConfig: null,
545
+ rateLimits: null
546
+ };
547
+ /**
548
+ * Initialize the client. Two-phase startup:
549
+ *
550
+ * Phase 1 (headed): Cloudflare Turnstile cannot be solved by headless browsers.
551
+ * A brief VISIBLE browser session navigates to Perplexity, auto-solves the CF
552
+ * challenge, and fetches all account info endpoints (models, ASI access, etc.)
553
+ * while Cloudflare isn't blocking. Then closes.
554
+ *
555
+ * Phase 2 (headless): Launches headless with the same persistent profile
556
+ * (now carrying fresh cf_clearance) for search operations.
557
+ *
558
+ * Set env PERPLEXITY_HEADLESS_ONLY=1 to skip the headed phase (uses disk cache).
559
+ */
560
+ async init() {
561
+ const activePaths = getActivePaths();
562
+ if (!existsSync(activePaths.browserData)) {
563
+ mkdirSync(activePaths.browserData, { recursive: true });
564
+ }
565
+ const browser = await resolveBrowserExecutable();
566
+ console.error(`[perplexity-mcp] Using ${browser.source}: ${browser.path}`);
567
+ const skipHeaded = process.env.PERPLEXITY_HEADLESS_ONLY === "1";
568
+ if (!skipHeaded) {
569
+ await this.headedBootstrap();
570
+ } else {
571
+ console.error("[perplexity-mcp] Skipping headed session (PERPLEXITY_HEADLESS_ONLY=1).");
572
+ this.loadCachedAccountInfo();
573
+ }
574
+ console.error("[perplexity-mcp] Launching headless browser...");
575
+ const launchOpts = buildLaunchOptions(true);
576
+ this.browser = await chromium.launch({
577
+ headless: launchOpts.headless,
578
+ args: launchOpts.args,
579
+ ...launchOpts.executablePath ? { executablePath: launchOpts.executablePath } : {},
580
+ ...launchOpts.channel ? { channel: launchOpts.channel } : {},
581
+ ignoreDefaultArgs: launchOpts.ignoreDefaultArgs
582
+ });
583
+ this.context = await getOrCreateContext(this.browser, {
584
+ viewport: launchOpts.viewport,
585
+ userAgent: launchOpts.userAgent
586
+ });
587
+ const saved = await getSavedCookies();
588
+ if (saved.length > 0) {
589
+ await this.context.addCookies(saved);
590
+ console.error(`[perplexity-mcp] Injected ${saved.length} saved cookies into browser context.`);
591
+ }
592
+ this.page = await this.context.newPage();
593
+ try {
594
+ await this.page.goto(PERPLEXITY_URL, { waitUntil: "domcontentloaded", timeout: 3e4 });
595
+ await this.page.waitForTimeout(2e3);
596
+ } catch (err) {
597
+ console.error("[perplexity-mcp] Navigation warning:", err.message);
598
+ }
599
+ await this.checkAuth();
600
+ if (!this.accountInfo.modelsConfig) {
601
+ await this.loadAccountInfo();
602
+ }
603
+ }
604
+ /**
605
+ * Brief VISIBLE browser session that:
606
+ * 1. Navigates to Perplexity to solve Cloudflare Turnstile (auto, no user interaction)
607
+ * 2. Fetches all account info endpoints while CF isn't blocking
608
+ * 3. Caches results to disk, then closes
609
+ */
610
+ async headedBootstrap() {
611
+ console.error("[perplexity-mcp] Starting headed bootstrap (solving CF + loading account info)...");
612
+ let ctx = null;
613
+ try {
614
+ const browserData = getActivePaths().browserData;
615
+ clearStaleSingletonLocks(browserData);
616
+ ctx = await chromium.launchPersistentContext(browserData, buildLaunchOptions(false));
617
+ const page = ctx.pages()[0] || await ctx.newPage();
618
+ await page.goto(PERPLEXITY_URL, { waitUntil: "domcontentloaded", timeout: 3e4 });
619
+ let cfResolved = false;
620
+ for (let i = 0; i < 20; i++) {
621
+ await page.waitForTimeout(1e3);
622
+ try {
623
+ const title = await page.title();
624
+ if (!title.includes("Just a moment")) {
625
+ cfResolved = true;
626
+ console.error(`[perplexity-mcp] Cloudflare resolved in ${i + 1}s.`);
627
+ break;
628
+ }
629
+ } catch {
630
+ console.error("[perplexity-mcp] CF redirect detected, waiting for page to settle...");
631
+ try {
632
+ await page.waitForLoadState("domcontentloaded", { timeout: 1e4 });
633
+ } catch {
634
+ }
635
+ cfResolved = true;
636
+ console.error(`[perplexity-mcp] Cloudflare resolved (via redirect) in ${i + 1}s.`);
637
+ break;
638
+ }
639
+ }
640
+ if (!cfResolved) {
641
+ console.error("[perplexity-mcp] CF challenge did not resolve in 20s \u2014 will use cached data.");
642
+ await ctx.close();
643
+ this.loadCachedAccountInfo();
644
+ return;
645
+ }
646
+ const authData = await page.evaluate(async (url) => {
647
+ try {
648
+ const r = await fetch(url, { credentials: "include" });
649
+ return await r.json();
650
+ } catch {
651
+ return null;
652
+ }
653
+ }, AUTH_SESSION_ENDPOINT);
654
+ this.userId = authData?.user?.id ?? null;
655
+ this.authenticated = !!this.userId;
656
+ if (this.authenticated) {
657
+ console.error(`[perplexity-mcp] Authenticated as user: ${this.userId}`);
658
+ } else {
659
+ console.error("[perplexity-mcp] Not authenticated (anonymous mode).");
660
+ }
661
+ if (this.authenticated) {
662
+ const fetchOk = async (url) => {
663
+ try {
664
+ const r = await fetch(url, { credentials: "include" });
665
+ return r.ok ? await r.json() : null;
666
+ } catch {
667
+ return null;
668
+ }
669
+ };
670
+ const modelsData = await page.evaluate(fetchOk, MODELS_CONFIG_ENDPOINT);
671
+ const asiData = await page.evaluate(fetchOk, ASI_ACCESS_ENDPOINT);
672
+ const rateLimitData = await page.evaluate(fetchOk, RATE_LIMIT_ENDPOINT);
673
+ const experimentsData = await page.evaluate(fetchOk, EXPERIMENTS_ENDPOINT);
674
+ if (modelsData) {
675
+ this.accountInfo.modelsConfig = modelsData;
676
+ const count = Object.keys(this.accountInfo.modelsConfig.models || {}).length;
677
+ console.error(`[perplexity-mcp] Loaded ${count} models from account.`);
678
+ }
679
+ if (asiData) {
680
+ const asi = asiData;
681
+ this.accountInfo.canUseComputer = asi.can_use_computer;
682
+ console.error(`[perplexity-mcp] Computer mode: ${asi.can_use_computer ? "available" : "not available"}`);
683
+ }
684
+ if (rateLimitData) {
685
+ this.accountInfo.rateLimits = rateLimitData;
686
+ }
687
+ if (experimentsData) {
688
+ const flags = deriveTierFlagsFromExperiments(
689
+ experimentsData,
690
+ this.accountInfo.canUseComputer
691
+ );
692
+ this.accountInfo.isPro = flags.isPro;
693
+ this.accountInfo.isMax = flags.isMax;
694
+ this.accountInfo.isEnterprise = flags.isEnterprise;
695
+ const tier = this.accountInfo.isMax ? "Max" : this.accountInfo.isPro ? "Pro" : this.accountInfo.isEnterprise ? "Enterprise" : "Free";
696
+ console.error(`[perplexity-mcp] Account tier: ${tier}`);
697
+ }
698
+ if (this.accountInfo.modelsConfig) {
699
+ try {
700
+ writeFileSync(getModelsCacheFile(), JSON.stringify(this.accountInfo, null, 2));
701
+ console.error("[perplexity-mcp] Account info cached to disk.");
702
+ } catch {
703
+ }
704
+ }
705
+ }
706
+ await ctx.close();
707
+ ctx = null;
708
+ console.error("[perplexity-mcp] Headed bootstrap complete.");
709
+ } catch (err) {
710
+ console.error("[perplexity-mcp] Headed bootstrap error:", err.message);
711
+ if (ctx) await ctx.close().catch(() => {
712
+ });
713
+ this.loadCachedAccountInfo();
714
+ }
715
+ }
716
+ async checkAuth() {
717
+ if (!this.page) return;
718
+ try {
719
+ const data = await this.page.evaluate(async (url) => {
720
+ const r = await fetch(url, { credentials: "include" });
721
+ return r.json();
722
+ }, AUTH_SESSION_ENDPOINT);
723
+ this.userId = data?.user?.id ?? null;
724
+ this.authenticated = !!this.userId;
725
+ if (this.authenticated) {
726
+ console.error(`[perplexity-mcp] Authenticated as user: ${this.userId}`);
727
+ } else {
728
+ console.error("[perplexity-mcp] No authenticated session (anonymous mode)");
729
+ }
730
+ } catch (err) {
731
+ console.error("[perplexity-mcp] Auth check failed:", err.message);
732
+ this.authenticated = false;
733
+ }
734
+ }
735
+ /**
736
+ * Load dynamic account info: models config, ASI access, rate limits, experiment flags.
737
+ * Falls back to disk cache if Cloudflare blocks the /rest/* endpoints.
738
+ */
739
+ async loadAccountInfo() {
740
+ if (!this.page || !this.authenticated) return;
741
+ const fetchEndpoint = async (url) => {
742
+ try {
743
+ return await this.page.evaluate(
744
+ async (u) => {
745
+ const r = await fetch(u, { credentials: "include" });
746
+ if (!r.ok) return null;
747
+ return r.json();
748
+ },
749
+ url
750
+ );
751
+ } catch {
752
+ return null;
753
+ }
754
+ };
755
+ let gotLiveData = false;
756
+ try {
757
+ const [modelsData, asiData, rateLimitData, experimentsData] = await Promise.all([
758
+ fetchEndpoint(MODELS_CONFIG_ENDPOINT),
759
+ fetchEndpoint(ASI_ACCESS_ENDPOINT),
760
+ fetchEndpoint(RATE_LIMIT_ENDPOINT),
761
+ fetchEndpoint(EXPERIMENTS_ENDPOINT)
762
+ ]);
763
+ if (modelsData) {
764
+ this.accountInfo.modelsConfig = modelsData;
765
+ const modelCount = Object.keys(this.accountInfo.modelsConfig.models || {}).length;
766
+ console.error(`[perplexity-mcp] Loaded ${modelCount} models from account`);
767
+ gotLiveData = true;
768
+ }
769
+ if (asiData) {
770
+ const asi = asiData;
771
+ this.accountInfo.canUseComputer = asi.can_use_computer;
772
+ console.error(`[perplexity-mcp] Computer mode: ${asi.can_use_computer ? "available" : "not available"}`);
773
+ gotLiveData = true;
774
+ }
775
+ if (rateLimitData) {
776
+ this.accountInfo.rateLimits = rateLimitData;
777
+ }
778
+ if (experimentsData) {
779
+ const flags = deriveTierFlagsFromExperiments(
780
+ experimentsData,
781
+ this.accountInfo.canUseComputer
782
+ );
783
+ this.accountInfo.isPro = flags.isPro;
784
+ this.accountInfo.isMax = flags.isMax;
785
+ this.accountInfo.isEnterprise = flags.isEnterprise;
786
+ const tier = this.accountInfo.isMax ? "Max" : this.accountInfo.isPro ? "Pro" : this.accountInfo.isEnterprise ? "Enterprise" : "Free";
787
+ console.error(`[perplexity-mcp] Account tier: ${tier}`);
788
+ gotLiveData = true;
789
+ }
790
+ } catch (err) {
791
+ console.error("[perplexity-mcp] Failed to load account info:", err.message);
792
+ }
793
+ if (gotLiveData) {
794
+ try {
795
+ writeFileSync(getModelsCacheFile(), JSON.stringify(this.accountInfo, null, 2));
796
+ console.error("[perplexity-mcp] Account info cached to disk.");
797
+ } catch {
798
+ }
799
+ } else {
800
+ this.loadCachedAccountInfo();
801
+ }
802
+ }
803
+ /**
804
+ * Load cached account info from disk (fallback when Cloudflare blocks).
805
+ */
806
+ loadCachedAccountInfo() {
807
+ const modelsCacheFile = getModelsCacheFile();
808
+ if (!existsSync(modelsCacheFile)) {
809
+ console.error("[perplexity-mcp] No cached account info found.");
810
+ return;
811
+ }
812
+ try {
813
+ const cached = JSON.parse(readFileSync(modelsCacheFile, "utf-8"));
814
+ this.accountInfo = cached;
815
+ const modelCount = cached.modelsConfig ? Object.keys(cached.modelsConfig.models || {}).length : 0;
816
+ console.error(`[perplexity-mcp] Loaded ${modelCount} models from disk cache.`);
817
+ } catch {
818
+ console.error("[perplexity-mcp] Failed to read cached account info.");
819
+ }
820
+ }
821
+ /**
822
+ * Removed in 0.3.0. Login now runs in a separate child process (login-runner)
823
+ * driven by AuthManager / the CLI so the long-lived MCP server doesn't hold
824
+ * the browser profile lock. After a successful login, the runner writes to
825
+ * the vault and drops a `.reinit` sentinel which the MCP server's watcher
826
+ * picks up to reload cookies via `reinit()`.
827
+ */
828
+ async loginViaBrowser(_opts = {}) {
829
+ throw new Error(
830
+ "loginViaBrowser is removed in 0.3.0. Call AuthManager.login() from the extension or `npx perplexity-user-mcp login` from the CLI."
831
+ );
832
+ }
833
+ /**
834
+ * Close the current browser context and re-run init() so freshly-written
835
+ * vault cookies are picked up. Called by the `.reinit` sentinel watcher
836
+ * after a child login-runner completes.
837
+ */
838
+ async reinit() {
839
+ console.error("[perplexity-mcp] Reinit requested \u2014 closing current context and reloading cookies.");
840
+ await this.shutdown().catch(() => {
841
+ });
842
+ this.browser = null;
843
+ this.context = null;
844
+ this.page = null;
845
+ this.authenticated = false;
846
+ this.userId = null;
847
+ await this.init();
848
+ }
849
+ async search(opts) {
850
+ const {
851
+ query,
852
+ modelPreference = "turbo",
853
+ mode = "concise",
854
+ sources = ["web"],
855
+ language = "en-US",
856
+ followUp
857
+ } = opts;
858
+ if (!this.page && isExperimentalImpitSearchEnabled()) {
859
+ const fast = await searchPerplexityViaImpit({ query, modelPreference, mode, sources, language, followUp });
860
+ if (fast) return fast;
861
+ }
862
+ if (!this.page) {
863
+ throw new Error("Client not initialized. Call init() first.");
864
+ }
865
+ if (modelPreference && this.accountInfo.modelsConfig) {
866
+ const mc = this.accountInfo.modelsConfig;
867
+ const knownIds = new Set(Object.keys(mc.models || {}));
868
+ for (const entry of mc.config || []) {
869
+ if (entry.reasoning_model) knownIds.add(entry.reasoning_model);
870
+ if (entry.non_reasoning_model) knownIds.add(entry.non_reasoning_model);
871
+ }
872
+ for (const def of Object.values(mc.default_models || {})) {
873
+ if (def) knownIds.add(def);
874
+ }
875
+ if (!knownIds.has(modelPreference)) {
876
+ throw new Error(
877
+ `Invalid model: "${modelPreference}". Use perplexity_models to see all available models.`
878
+ );
879
+ }
880
+ }
881
+ const { body, requestId } = buildSearchRequest({ query, modelPreference, mode, sources, language, followUp });
882
+ const rawResponse = await this.page.evaluate(
883
+ async ({ url, requestBody, reqId }) => {
884
+ const resp = await fetch(url, {
885
+ method: "POST",
886
+ credentials: "include",
887
+ headers: {
888
+ "accept": "text/event-stream",
889
+ "content-type": "application/json",
890
+ "x-perplexity-request-reason": "perplexity-query-state-provider",
891
+ "x-request-id": reqId
892
+ },
893
+ body: requestBody
894
+ });
895
+ if (!resp.ok) {
896
+ const text = await resp.text().catch(() => "");
897
+ throw new Error(`Perplexity request failed: ${resp.status} ${text.slice(0, 300)}`);
898
+ }
899
+ return await resp.text();
900
+ },
901
+ { url: QUERY_ENDPOINT, requestBody: JSON.stringify(body), reqId: requestId }
902
+ );
903
+ return _PerplexityClient.parseSSEText(rawResponse);
904
+ }
905
+ /**
906
+ * Submit an ASI/Computer mode task and wait for it to complete.
907
+ *
908
+ * Flow (discovered from browser HAR capture):
909
+ * 1. POST /rest/sse/perplexity_ask → initial SSE with status: PENDING + backend_uuid
910
+ * 2. POST /rest/sse/perplexity_ask/reconnect/{backend_uuid} → SSE stream with
911
+ * progressive updates until status: COMPLETED + step_type: FINAL
912
+ * 3. Parse FINAL event blocks: workflow_block (answer text + workflow sources),
913
+ * web_result_block (citation sources), pending_followups_block
914
+ */
915
+ async computeASI(opts) {
916
+ if (!this.page) {
917
+ throw new Error("Client not initialized. Call init() first.");
918
+ }
919
+ const {
920
+ query,
921
+ modelPreference = "pplx_asi",
922
+ language = "en-US",
923
+ timeoutMs = 18e4
924
+ } = opts;
925
+ if (!query || !query.trim()) {
926
+ throw new Error("Query cannot be empty. Please provide a question or task for Computer mode.");
927
+ }
928
+ const frontendUuid = randomUUID();
929
+ const frontendContextUuid = randomUUID();
930
+ const requestId = randomUUID();
931
+ const params = {
932
+ attachments: [],
933
+ language,
934
+ timezone: "America/Los_Angeles",
935
+ search_focus: "internet",
936
+ sources: ["web"],
937
+ search_recency_filter: null,
938
+ frontend_uuid: frontendUuid,
939
+ mode: "copilot",
940
+ model_preference: modelPreference,
941
+ is_related_query: false,
942
+ is_sponsored: false,
943
+ frontend_context_uuid: frontendContextUuid,
944
+ prompt_source: "user",
945
+ query_source: "home",
946
+ is_incognito: false,
947
+ time_from_first_type: 5e3 + Math.floor(Math.random() * 15e3),
948
+ local_search_enabled: false,
949
+ use_schematized_api: true,
950
+ send_back_text_in_streaming_api: false,
951
+ supported_block_use_cases: SUPPORTED_BLOCK_USE_CASES,
952
+ client_coordinates: null,
953
+ mentions: [],
954
+ dsl_query: query,
955
+ skip_search_enabled: true,
956
+ is_nav_suggestions_disabled: false,
957
+ source: "default",
958
+ always_search_override: false,
959
+ override_no_search: false,
960
+ should_ask_for_mcp_tool_confirmation: true,
961
+ browser_agent_allow_once_from_toggle: false,
962
+ force_enable_browser_agent: false,
963
+ supported_features: ["browser_agent_permission_banner_v1.1"],
964
+ version: "2.18"
965
+ };
966
+ const body = { params, query_str: query };
967
+ const rawSSE = await this.page.evaluate(
968
+ async ({ url, requestBody, reqId }) => {
969
+ const resp = await fetch(url, {
970
+ method: "POST",
971
+ credentials: "include",
972
+ headers: {
973
+ "accept": "text/event-stream",
974
+ "content-type": "application/json",
975
+ "x-perplexity-request-reason": "perplexity-query-state-provider",
976
+ "x-request-id": reqId
977
+ },
978
+ body: requestBody
979
+ });
980
+ if (!resp.ok) {
981
+ const text = await resp.text().catch(() => "");
982
+ throw new Error(`ASI submit failed: ${resp.status} ${text.slice(0, 300)}`);
983
+ }
984
+ return await resp.text();
985
+ },
986
+ { url: QUERY_ENDPOINT, requestBody: JSON.stringify(body), reqId: requestId }
987
+ );
988
+ const dataLine = rawSSE.split("\n").find((l) => l.startsWith("data: "));
989
+ if (!dataLine) throw new Error("No data in ASI response");
990
+ const initialData = JSON.parse(dataLine.slice(6));
991
+ const threadSlug = initialData.thread_url_slug;
992
+ const backendUuid = initialData.backend_uuid;
993
+ const readWriteToken = initialData.read_write_token;
994
+ if (!backendUuid) throw new Error("No backend_uuid in ASI response");
995
+ console.error(`[perplexity-mcp] ASI task submitted: ${threadSlug || backendUuid} (reconnecting for result...)`);
996
+ const reconnectUrl = `${QUERY_ENDPOINT}/reconnect/${backendUuid}`;
997
+ const startedAt = Date.now();
998
+ const deadline = startedAt + timeoutMs;
999
+ this.page.setDefaultTimeout(timeoutMs + 3e4);
1000
+ let reconnectAttempt = 0;
1001
+ let lastSSEData = null;
1002
+ while (Date.now() < deadline - 5e3) {
1003
+ reconnectAttempt++;
1004
+ const remaining = deadline - Date.now();
1005
+ console.error(`[perplexity-mcp] ASI reconnect #${reconnectAttempt} (${Math.round(remaining / 1e3)}s remaining)...`);
1006
+ try {
1007
+ const sseText = await this.page.evaluate(
1008
+ async ({ url, timeoutMs: timeoutMs2 }) => {
1009
+ const controller = new AbortController();
1010
+ const timer = setTimeout(() => controller.abort(), timeoutMs2);
1011
+ try {
1012
+ const resp = await fetch(url, {
1013
+ method: "POST",
1014
+ credentials: "include",
1015
+ signal: controller.signal,
1016
+ headers: {
1017
+ "accept": "text/event-stream",
1018
+ "content-type": "application/json",
1019
+ "x-perplexity-request-reason": "reconnect-stream",
1020
+ "x-request-id": crypto.randomUUID()
1021
+ },
1022
+ body: JSON.stringify({ reconnectInitialSnapshot: true })
1023
+ });
1024
+ if (!resp.ok) {
1025
+ const errText = await resp.text().catch(() => "");
1026
+ return `ERROR:${resp.status}:${errText.slice(0, 500)}`;
1027
+ }
1028
+ const reader = resp.body.getReader();
1029
+ const decoder = new TextDecoder();
1030
+ let accumulated = "";
1031
+ let completedSeen = false;
1032
+ while (true) {
1033
+ let readResult;
1034
+ if (completedSeen) {
1035
+ const read = reader.read();
1036
+ const timeout = new Promise(
1037
+ (r) => setTimeout(() => r({ done: true, value: void 0 }), 5e3)
1038
+ );
1039
+ readResult = await Promise.race([read, timeout]);
1040
+ } else {
1041
+ readResult = await reader.read();
1042
+ }
1043
+ if (readResult.done) break;
1044
+ accumulated += decoder.decode(readResult.value, { stream: true });
1045
+ if (!completedSeen && accumulated.includes('"status":"COMPLETED"')) {
1046
+ completedSeen = true;
1047
+ }
1048
+ if (accumulated.includes("event:end_of_stream") || accumulated.includes("event: end_of_stream")) {
1049
+ break;
1050
+ }
1051
+ }
1052
+ reader.cancel().catch(() => {
1053
+ });
1054
+ return accumulated;
1055
+ } catch (e) {
1056
+ if (e.name === "AbortError") return "ERROR:TIMEOUT:stream timeout";
1057
+ return `ERROR:FETCH:${e.message || e}`;
1058
+ } finally {
1059
+ clearTimeout(timer);
1060
+ }
1061
+ },
1062
+ { url: reconnectUrl, timeoutMs: Math.min(remaining, 9e4) }
1063
+ );
1064
+ if (sseText.startsWith("ERROR:")) {
1065
+ console.error(`[perplexity-mcp] ASI reconnect #${reconnectAttempt}: ${sseText.slice(0, 100)}`);
1066
+ await this.page.waitForTimeout(8e3);
1067
+ continue;
1068
+ }
1069
+ if (sseText.length > (lastSSEData?.length || 0)) {
1070
+ lastSSEData = sseText;
1071
+ }
1072
+ const result = _PerplexityClient.parseASIReconnectSSE(sseText, threadSlug, backendUuid, readWriteToken);
1073
+ if (!result.answer.startsWith("ASI task may still be running")) {
1074
+ const elapsed = Math.round((Date.now() - startedAt) / 1e3);
1075
+ console.error(`[perplexity-mcp] ASI completed via reconnect #${reconnectAttempt} (${elapsed}s).`);
1076
+ this.page.setDefaultTimeout(3e4);
1077
+ if (result.files?.length) {
1078
+ await this.downloadASIFiles(result.files, threadSlug);
1079
+ }
1080
+ return result;
1081
+ }
1082
+ console.error(`[perplexity-mcp] ASI reconnect #${reconnectAttempt}: stream ended without COMPLETED (${sseText.length} chars), retrying in 5s...`);
1083
+ await this.page.waitForTimeout(5e3);
1084
+ } catch (err) {
1085
+ console.error(`[perplexity-mcp] ASI reconnect #${reconnectAttempt} error: ${err.message?.slice(0, 150)}`);
1086
+ await this.page.waitForTimeout(8e3);
1087
+ }
1088
+ }
1089
+ console.error(`[perplexity-mcp] ASI reconnect loop exhausted. Attempting fallback extraction...`);
1090
+ if (lastSSEData) {
1091
+ const result = _PerplexityClient.extractFromWorkflowBlock(lastSSEData, threadSlug, backendUuid, readWriteToken);
1092
+ if (result) {
1093
+ this.page.setDefaultTimeout(3e4);
1094
+ return result;
1095
+ }
1096
+ }
1097
+ if (threadSlug) {
1098
+ try {
1099
+ const rawJson = await this.page.evaluate(
1100
+ async (url) => {
1101
+ const resp = await fetch(url, { credentials: "include" });
1102
+ return await resp.text();
1103
+ },
1104
+ THREAD_ENDPOINT(threadSlug)
1105
+ );
1106
+ const threadData = JSON.parse(rawJson);
1107
+ if (threadData.status === "success") {
1108
+ const entries = threadData.entries || [];
1109
+ const lastEntry = entries[entries.length - 1];
1110
+ if (lastEntry) {
1111
+ return _PerplexityClient.parseASIThreadEntry(lastEntry, threadSlug, backendUuid, readWriteToken);
1112
+ }
1113
+ }
1114
+ } catch (err) {
1115
+ console.error(`[perplexity-mcp] ASI thread fallback error: ${err.message?.slice(0, 100)}`);
1116
+ }
1117
+ }
1118
+ this.page.setDefaultTimeout(3e4);
1119
+ console.error(`[perplexity-mcp] ASI task timed out after ${timeoutMs / 1e3}s.`);
1120
+ return {
1121
+ answer: `ASI task timed out after ${timeoutMs / 1e3}s. The task may still be running.
1122
+ View results at: ${PERPLEXITY_URL}/search/${threadSlug}`,
1123
+ sources: [],
1124
+ media: [],
1125
+ suggestedFollowups: [],
1126
+ threadUrl: `${PERPLEXITY_URL}/search/${threadSlug}`
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Re-fetch results from a Perplexity thread that may have completed after we timed out.
1131
+ * Tries reconnect SSE first (using backendUuid), then falls back to thread endpoint.
1132
+ */
1133
+ async retrieveThread(opts) {
1134
+ const { threadSlug, backendUuid, readWriteToken } = opts;
1135
+ if (!this.page) {
1136
+ const fast = await retrieveThreadViaImpit({ threadSlug, backendUuid: backendUuid ?? null, readWriteToken: readWriteToken ?? null });
1137
+ if (fast && (!fast.files || fast.files.length === 0)) return fast;
1138
+ }
1139
+ if (!this.page) {
1140
+ await this.init();
1141
+ }
1142
+ if (!this.page) {
1143
+ throw new Error("Client not initialized after init().");
1144
+ }
1145
+ if (backendUuid) {
1146
+ try {
1147
+ const reconnectUrl = `${QUERY_ENDPOINT}/reconnect/${backendUuid}`;
1148
+ console.error(`[perplexity-mcp] Retrieving via reconnect: ${backendUuid}`);
1149
+ const sseText = await this.page.evaluate(
1150
+ async ({ url }) => {
1151
+ const controller = new AbortController();
1152
+ const timer = setTimeout(() => controller.abort(), 3e4);
1153
+ try {
1154
+ const resp = await fetch(url, {
1155
+ method: "POST",
1156
+ credentials: "include",
1157
+ signal: controller.signal,
1158
+ headers: {
1159
+ "accept": "text/event-stream",
1160
+ "content-type": "application/json",
1161
+ "x-perplexity-request-reason": "reconnect-stream",
1162
+ "x-request-id": crypto.randomUUID()
1163
+ },
1164
+ body: JSON.stringify({ reconnectInitialSnapshot: true })
1165
+ });
1166
+ if (!resp.ok) return `ERROR:${resp.status}`;
1167
+ const reader = resp.body.getReader();
1168
+ const decoder = new TextDecoder();
1169
+ let accumulated = "";
1170
+ let completedSeen = false;
1171
+ while (true) {
1172
+ let readResult;
1173
+ if (completedSeen) {
1174
+ const read = reader.read();
1175
+ const timeout = new Promise(
1176
+ (r) => setTimeout(() => r({ done: true, value: void 0 }), 5e3)
1177
+ );
1178
+ readResult = await Promise.race([read, timeout]);
1179
+ } else {
1180
+ readResult = await reader.read();
1181
+ }
1182
+ if (readResult.done) break;
1183
+ accumulated += decoder.decode(readResult.value, { stream: true });
1184
+ if (!completedSeen && accumulated.includes('"status":"COMPLETED"')) {
1185
+ completedSeen = true;
1186
+ }
1187
+ if (accumulated.includes("event:end_of_stream") || accumulated.includes("event: end_of_stream")) {
1188
+ break;
1189
+ }
1190
+ }
1191
+ reader.cancel().catch(() => {
1192
+ });
1193
+ return accumulated;
1194
+ } catch (e) {
1195
+ if (e.name === "AbortError") return "ERROR:TIMEOUT";
1196
+ return `ERROR:FETCH:${e.message || e}`;
1197
+ } finally {
1198
+ clearTimeout(timer);
1199
+ }
1200
+ },
1201
+ { url: reconnectUrl }
1202
+ );
1203
+ if (!sseText.startsWith("ERROR:")) {
1204
+ const result = _PerplexityClient.parseASIReconnectSSE(sseText, threadSlug, backendUuid, readWriteToken ?? "");
1205
+ if (!result.answer.startsWith("ASI task may still be running")) {
1206
+ console.error(`[perplexity-mcp] Retrieved completed result via reconnect.`);
1207
+ if (result.files?.length) {
1208
+ await this.downloadASIFiles(result.files, threadSlug);
1209
+ }
1210
+ return result;
1211
+ }
1212
+ const wbResult = _PerplexityClient.extractFromWorkflowBlock(sseText, threadSlug, backendUuid, readWriteToken ?? "");
1213
+ if (wbResult) {
1214
+ console.error(`[perplexity-mcp] Retrieved result via workflow block extraction.`);
1215
+ return wbResult;
1216
+ }
1217
+ }
1218
+ } catch (err) {
1219
+ console.error(`[perplexity-mcp] Reconnect retrieval failed: ${err.message?.slice(0, 100)}`);
1220
+ }
1221
+ }
1222
+ if (threadSlug) {
1223
+ console.error(`[perplexity-mcp] Retrieving via thread endpoint: ${threadSlug}`);
1224
+ try {
1225
+ const rawJson = await this.page.evaluate(
1226
+ async (url) => {
1227
+ const resp = await fetch(url, { credentials: "include" });
1228
+ return await resp.text();
1229
+ },
1230
+ THREAD_ENDPOINT(threadSlug)
1231
+ );
1232
+ const threadData = JSON.parse(rawJson);
1233
+ if (threadData.status === "success") {
1234
+ const entries = threadData.entries || [];
1235
+ const lastEntry = entries[entries.length - 1];
1236
+ if (lastEntry) {
1237
+ const result = _PerplexityClient.parseASIThreadEntry(lastEntry, threadSlug, backendUuid ?? "", readWriteToken ?? "");
1238
+ console.error(`[perplexity-mcp] Retrieved result via thread endpoint.`);
1239
+ return result;
1240
+ }
1241
+ }
1242
+ return {
1243
+ answer: `Thread exists but task is still running. Try again later.
1244
+ View at: ${PERPLEXITY_URL}/search/${threadSlug}`,
1245
+ sources: [],
1246
+ media: [],
1247
+ suggestedFollowups: [],
1248
+ threadUrl: `${PERPLEXITY_URL}/search/${threadSlug}`
1249
+ };
1250
+ } catch (err) {
1251
+ throw new Error(`Failed to retrieve thread ${threadSlug}: ${err.message}`);
1252
+ }
1253
+ }
1254
+ throw new Error("No threadSlug or backendUuid provided for retrieval.");
1255
+ }
1256
+ /**
1257
+ * Parse the reconnect SSE stream from an ASI task.
1258
+ *
1259
+ * The SSE stream contains progressive events. The FINAL event has:
1260
+ * - status: COMPLETED, step_type: FINAL, text_completed: true
1261
+ * - blocks[]: workflow_root (workflow_block with steps/items/text),
1262
+ * web_results (web_result_block), pending_followups, sources_answer_mode
1263
+ */
1264
+ static parseASIReconnectSSE(sseText, threadSlug, backendUuid, readWriteToken) {
1265
+ const events = sseText.split(/\r?\n\r?\n/);
1266
+ let finalData = null;
1267
+ for (const event of events) {
1268
+ let dataIdx = event.indexOf("data: ");
1269
+ let dataOffset = 6;
1270
+ if (dataIdx === -1) {
1271
+ dataIdx = event.indexOf("data:");
1272
+ dataOffset = 5;
1273
+ }
1274
+ if (dataIdx === -1) continue;
1275
+ const jsonStr = event.slice(dataIdx + dataOffset);
1276
+ try {
1277
+ const data = JSON.parse(jsonStr);
1278
+ if (data.status === "COMPLETED" || data.step_type === "FINAL") {
1279
+ finalData = data;
1280
+ }
1281
+ if (!threadSlug && data.thread_url_slug) {
1282
+ threadSlug = data.thread_url_slug;
1283
+ }
1284
+ } catch {
1285
+ }
1286
+ }
1287
+ const allBlockUsages = /* @__PURE__ */ new Set();
1288
+ const allItemTypes = /* @__PURE__ */ new Set();
1289
+ const allPayloadTypes = /* @__PURE__ */ new Set();
1290
+ for (const event of events) {
1291
+ let di = event.indexOf("data: ");
1292
+ let doff = 6;
1293
+ if (di === -1) {
1294
+ di = event.indexOf("data:");
1295
+ doff = 5;
1296
+ }
1297
+ if (di === -1) continue;
1298
+ try {
1299
+ const d = JSON.parse(event.slice(di + doff));
1300
+ for (const b of d.blocks || []) {
1301
+ allBlockUsages.add(b.intended_usage || "?");
1302
+ if (b.workflow_block?.steps) {
1303
+ for (const s of b.workflow_block.steps) {
1304
+ for (const item of s.items || []) {
1305
+ allItemTypes.add(item.type || "?");
1306
+ if (item.payload) for (const [k, v] of Object.entries(item.payload)) {
1307
+ if (v != null) allPayloadTypes.add(k);
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ if (b.diff_block?.patches) {
1313
+ for (const p of b.diff_block.patches) {
1314
+ if (p.value?.type) allItemTypes.add(p.value.type);
1315
+ if (p.value?.payload) for (const [k, v] of Object.entries(p.value.payload)) {
1316
+ if (v != null) allPayloadTypes.add(k);
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+ } catch {
1322
+ }
1323
+ }
1324
+ console.error(`[perplexity-mcp] ASI blocks: ${[...allBlockUsages].join(", ")}`);
1325
+ console.error(`[perplexity-mcp] ASI item types: ${[...allItemTypes].join(", ")}`);
1326
+ console.error(`[perplexity-mcp] ASI payload types: ${[...allPayloadTypes].join(", ")}`);
1327
+ if (!finalData) {
1328
+ console.error("[perplexity-mcp] ASI: No FINAL event found in reconnect stream.");
1329
+ return {
1330
+ answer: `ASI task may still be running. View results at: ${PERPLEXITY_URL}/search/${threadSlug}`,
1331
+ sources: [],
1332
+ media: [],
1333
+ suggestedFollowups: [],
1334
+ threadUrl: threadSlug ? `${PERPLEXITY_URL}/search/${threadSlug}` : void 0
1335
+ };
1336
+ }
1337
+ console.error(`[perplexity-mcp] ASI task completed (status=${finalData.status}).`);
1338
+ const blocks = finalData.blocks || [];
1339
+ let answer = "";
1340
+ const webSources = [];
1341
+ const followups = [];
1342
+ const files = [];
1343
+ for (const block of blocks) {
1344
+ if (block.intended_usage === "workflow_root" && block.workflow_block) {
1345
+ const wb = block.workflow_block;
1346
+ const steps = wb.steps || [];
1347
+ for (const step of steps) {
1348
+ for (const item of step.items || []) {
1349
+ const payload = item.payload || {};
1350
+ if (payload.text_payload?.text) {
1351
+ if (answer) answer += "\n\n";
1352
+ answer += payload.text_payload.text;
1353
+ }
1354
+ if (payload.sources_payload?.sources) {
1355
+ for (const src of payload.sources_payload.sources) {
1356
+ webSources.push({
1357
+ title: src.name ?? "",
1358
+ url: src.url ?? "",
1359
+ snippet: src.snippet ?? ""
1360
+ });
1361
+ }
1362
+ }
1363
+ }
1364
+ }
1365
+ }
1366
+ if (block.intended_usage === "web_results" && block.web_result_block?.web_results) {
1367
+ if (webSources.length === 0) {
1368
+ for (const wr of block.web_result_block.web_results) {
1369
+ webSources.push({
1370
+ title: wr.name ?? "",
1371
+ url: wr.url ?? "",
1372
+ snippet: wr.snippet ?? ""
1373
+ });
1374
+ }
1375
+ }
1376
+ }
1377
+ if (block.intended_usage === "assets_answer_mode" && block.assets_mode_block?.assets) {
1378
+ const seenUrls = /* @__PURE__ */ new Set();
1379
+ for (const asset of block.assets_mode_block.assets) {
1380
+ if (asset.download_info) {
1381
+ for (const dl of asset.download_info) {
1382
+ if (dl.url && dl.filename && !seenUrls.has(dl.url)) {
1383
+ seenUrls.add(dl.url);
1384
+ files.push({
1385
+ filename: dl.filename,
1386
+ assetType: asset.asset_type || "UNKNOWN",
1387
+ url: dl.url,
1388
+ size: dl.size || void 0,
1389
+ mediaType: dl.media_type || void 0
1390
+ });
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ if (block.intended_usage === "pending_followups" && block.pending_followups_block?.followups) {
1397
+ for (const f of block.pending_followups_block.followups) {
1398
+ if (typeof f === "string") followups.push(f);
1399
+ else if (f?.text) followups.push(f.text);
1400
+ }
1401
+ }
1402
+ }
1403
+ answer = answer.trim() || `ASI task completed. View full results at: ${PERPLEXITY_URL}/search/${threadSlug}`;
1404
+ if (files.length > 0) {
1405
+ console.error(`[perplexity-mcp] ASI: ${files.length} file(s) detected: ${files.map((f) => f.filename).join(", ")}`);
1406
+ }
1407
+ return {
1408
+ answer,
1409
+ sources: webSources,
1410
+ media: [],
1411
+ files: files.length > 0 ? files : void 0,
1412
+ suggestedFollowups: followups,
1413
+ threadUrl: threadSlug ? `${PERPLEXITY_URL}/search/${threadSlug}` : void 0,
1414
+ followUp: {
1415
+ backendUuid,
1416
+ readWriteToken,
1417
+ threadUrlSlug: threadSlug,
1418
+ threadTitle: finalData.thread_title || null
1419
+ }
1420
+ };
1421
+ }
1422
+ /**
1423
+ * Download files generated by ASI tasks.
1424
+ *
1425
+ * Uses Playwright's `APIRequestContext` (`context.request.get`) instead of
1426
+ * an in-page `fetch()`. ASI assets typically live on off-origin CDN buckets
1427
+ * (e.g. `pplx-res.cloudinary.com`, GCS, S3); fetching them from inside the
1428
+ * Perplexity origin would trip CORS unless the browser was launched with
1429
+ * `--disable-web-security` — which we no longer do (see STEALTH_ARGS note).
1430
+ *
1431
+ * `APIRequestContext` runs outside the page context, automatically inherits
1432
+ * cookies from the BrowserContext, and is not subject to the same-origin
1433
+ * policy, so it works for both same-origin and off-origin asset URLs.
1434
+ *
1435
+ * Files are saved to ~/.perplexity-mcp/downloads/<threadSlug>/.
1436
+ *
1437
+ * Public contract is preserved: this still mutates each `file.localPath`
1438
+ * in-place on success and silently skips on failure (logs to stderr).
1439
+ */
1440
+ async downloadASIFiles(files, threadSlug) {
1441
+ if (!this.context || files.length === 0) return;
1442
+ const downloadDir = join2(getConfigDir(), "downloads", threadSlug || "unknown");
1443
+ if (!existsSync(downloadDir)) {
1444
+ mkdirSync(downloadDir, { recursive: true });
1445
+ }
1446
+ for (const file of files) {
1447
+ if (!file.url) continue;
1448
+ try {
1449
+ console.error(`[perplexity-mcp] Downloading: ${file.filename} (${file.size ? Math.round(file.size / 1024) + "KB" : "unknown size"})...`);
1450
+ const response = await this.context.request.get(file.url, {
1451
+ // Conservative ceiling — assets are usually small (KB to a few MB).
1452
+ // Prevents an unresponsive CDN from stalling the MCP request loop.
1453
+ timeout: 6e4
1454
+ });
1455
+ if (!response.ok()) {
1456
+ console.error(`[perplexity-mcp] Download failed for ${file.filename}: ERROR:${response.status()}`);
1457
+ continue;
1458
+ }
1459
+ const body = await response.body();
1460
+ const filePath = join2(downloadDir, file.filename);
1461
+ writeFileSync(filePath, body);
1462
+ file.localPath = filePath;
1463
+ console.error(`[perplexity-mcp] Saved: ${filePath} (${body.length} bytes)`);
1464
+ } catch (err) {
1465
+ console.error(`[perplexity-mcp] Download error for ${file.filename}: ${err.message?.slice(0, 100)}`);
1466
+ }
1467
+ }
1468
+ }
1469
+ /**
1470
+ * Parse a completed ASI thread entry from GET /rest/thread/{slug}.
1471
+ * Used as fallback (Phase B) when streaming reconnect doesn't complete in time.
1472
+ *
1473
+ * Thread entry structure:
1474
+ * - plan.goals[].description — answer text (simple queries)
1475
+ * - text — JSON string of steps, FINAL step may have content.answer with full text + web_results
1476
+ * - step_type: "FINAL", status: "completed"
1477
+ */
1478
+ /**
1479
+ * Extract answer from the workflow_block in the last SSE snapshot.
1480
+ * Used when the reconnect loop didn't catch COMPLETED live but we have
1481
+ * a snapshot with workflow_block data (steps, items, sources).
1482
+ */
1483
+ static extractFromWorkflowBlock(sseText, threadSlug, backendUuid, readWriteToken) {
1484
+ const events = sseText.split(/\r?\n\r?\n/);
1485
+ let lastData = null;
1486
+ for (const event of events) {
1487
+ let di = event.indexOf("data: ");
1488
+ let doff = 6;
1489
+ if (di === -1) {
1490
+ di = event.indexOf("data:");
1491
+ doff = 5;
1492
+ }
1493
+ if (di === -1) continue;
1494
+ try {
1495
+ const d = JSON.parse(event.slice(di + doff));
1496
+ if (d.blocks) lastData = d;
1497
+ } catch {
1498
+ }
1499
+ }
1500
+ if (!lastData?.blocks) return null;
1501
+ const blocks = lastData.blocks;
1502
+ const wfBlock = blocks.find(
1503
+ (b) => b.intended_usage === "workflow_root"
1504
+ )?.workflow_block;
1505
+ if (!wfBlock?.steps) return null;
1506
+ const textParts = [];
1507
+ const webSources = [];
1508
+ const followups = [];
1509
+ for (const step of wfBlock.steps) {
1510
+ for (const item of step.items || []) {
1511
+ if (item.payload?.text_payload?.text) {
1512
+ textParts.push(item.payload.text_payload.text);
1513
+ }
1514
+ }
1515
+ }
1516
+ const wrBlock = blocks.find(
1517
+ (b) => b.intended_usage === "web_results"
1518
+ )?.web_result_block;
1519
+ if (wrBlock?.web_results) {
1520
+ for (const wr of wrBlock.web_results) {
1521
+ if (wr.url) {
1522
+ webSources.push({ title: wr.name ?? "", url: wr.url, snippet: wr.snippet ?? "" });
1523
+ }
1524
+ }
1525
+ }
1526
+ const pfBlock = blocks.find(
1527
+ (b) => b.intended_usage === "pending_followups"
1528
+ )?.pending_followups_block;
1529
+ if (pfBlock?.followups) {
1530
+ for (const f of pfBlock.followups) {
1531
+ if (typeof f === "string") followups.push(f);
1532
+ else if (f.text) followups.push(f.text);
1533
+ }
1534
+ }
1535
+ const answer = textParts.join("\n\n").trim();
1536
+ if (!answer && webSources.length === 0) return null;
1537
+ const threadUrl = threadSlug ? `${PERPLEXITY_URL}/search/${threadSlug}` : void 0;
1538
+ console.error(`[perplexity-mcp] ASI: extracted ${textParts.length} text fragments (${answer.length} chars) + ${webSources.length} sources from workflow_block.`);
1539
+ return {
1540
+ answer: answer || `ASI task completed. View full results at: ${threadUrl}`,
1541
+ sources: webSources,
1542
+ media: [],
1543
+ suggestedFollowups: followups,
1544
+ threadUrl,
1545
+ followUp: { backendUuid, readWriteToken, threadUrlSlug: threadSlug, threadTitle: null }
1546
+ };
1547
+ }
1548
+ static parseASIThreadEntry(entry, threadSlug, backendUuid, readWriteToken) {
1549
+ let answer = "";
1550
+ const webSources = [];
1551
+ const followups = [];
1552
+ try {
1553
+ const steps = JSON.parse(entry.text || "[]");
1554
+ const finalStep = steps.find((s) => s.step_type === "FINAL");
1555
+ if (finalStep?.content?.answer) {
1556
+ const answerData = typeof finalStep.content.answer === "string" ? JSON.parse(finalStep.content.answer) : finalStep.content.answer;
1557
+ if (answerData.text) answer = answerData.text;
1558
+ if (answerData.web_results) {
1559
+ for (const wr of answerData.web_results) {
1560
+ webSources.push({
1561
+ title: wr.name ?? "",
1562
+ url: wr.url ?? "",
1563
+ snippet: wr.snippet ?? ""
1564
+ });
1565
+ }
1566
+ }
1567
+ }
1568
+ } catch {
1569
+ }
1570
+ if (!answer && entry.plan?.goals) {
1571
+ const goals = entry.plan.goals;
1572
+ for (let i = goals.length - 1; i >= 0; i--) {
1573
+ if (goals[i].description && goals[i].description.trim()) {
1574
+ answer = goals[i].description;
1575
+ break;
1576
+ }
1577
+ }
1578
+ }
1579
+ if (!answer && entry.workflow_items) {
1580
+ for (const item of entry.workflow_items) {
1581
+ if (item.payload?.text_payload?.text) {
1582
+ if (answer) answer += "\n\n";
1583
+ answer += item.payload.text_payload.text;
1584
+ }
1585
+ }
1586
+ }
1587
+ answer = answer.trim() || `ASI task completed. View full results at: ${PERPLEXITY_URL}/search/${threadSlug}`;
1588
+ return {
1589
+ answer,
1590
+ sources: webSources,
1591
+ media: [],
1592
+ suggestedFollowups: followups,
1593
+ threadUrl: threadSlug ? `${PERPLEXITY_URL}/search/${threadSlug}` : void 0,
1594
+ followUp: {
1595
+ backendUuid,
1596
+ readWriteToken,
1597
+ threadUrlSlug: threadSlug,
1598
+ threadTitle: entry.thread_title || null
1599
+ }
1600
+ };
1601
+ }
1602
+ static parseSSEText(text) {
1603
+ let fullAnswer = "";
1604
+ let fullReasoning = "";
1605
+ const webSources = [];
1606
+ const media = [];
1607
+ const followups = [];
1608
+ let backendUuid = null;
1609
+ let readWriteToken = null;
1610
+ let threadUrlSlug = null;
1611
+ let threadTitle = null;
1612
+ const sectionTexts = /* @__PURE__ */ new Map();
1613
+ let summaryText = "";
1614
+ const events = text.split(/\r?\n\r?\n/);
1615
+ for (const event of events) {
1616
+ if (event.startsWith("event: end_of_stream")) break;
1617
+ if (!event.startsWith("event: message")) continue;
1618
+ const dataIdx = event.indexOf("data: ");
1619
+ if (dataIdx === -1) continue;
1620
+ let jsonData;
1621
+ try {
1622
+ jsonData = JSON.parse(event.slice(dataIdx + 6));
1623
+ } catch {
1624
+ continue;
1625
+ }
1626
+ if (jsonData.backend_uuid) backendUuid = jsonData.backend_uuid;
1627
+ if (jsonData.read_write_token && !readWriteToken)
1628
+ readWriteToken = jsonData.read_write_token;
1629
+ if (jsonData.thread_url_slug && !threadUrlSlug)
1630
+ threadUrlSlug = jsonData.thread_url_slug;
1631
+ if (jsonData.thread_title) threadTitle = jsonData.thread_title;
1632
+ if (jsonData.related_query_items) {
1633
+ for (const item of jsonData.related_query_items) {
1634
+ if (item.text) followups.push(item.text);
1635
+ }
1636
+ }
1637
+ for (const block of jsonData.blocks ?? []) {
1638
+ const intended = block.intended_usage ?? "";
1639
+ if (intended === "sources_answer_mode") {
1640
+ const results = block.sources_mode_block?.web_results ?? [];
1641
+ for (const r of results) {
1642
+ webSources.push({
1643
+ title: r.name ?? "",
1644
+ url: r.url ?? "",
1645
+ snippet: r.snippet ?? ""
1646
+ });
1647
+ }
1648
+ continue;
1649
+ }
1650
+ if (intended === "media_items") {
1651
+ for (const item of block.media_block?.media_items ?? []) {
1652
+ media.push({
1653
+ type: item.medium ?? "",
1654
+ url: item.url ?? "",
1655
+ name: item.name ?? ""
1656
+ });
1657
+ }
1658
+ continue;
1659
+ }
1660
+ const field = block.diff_block?.field ?? "";
1661
+ const isSection = /^ask_text_\d+_markdown$/.test(intended);
1662
+ const isSummary = intended === "ask_text";
1663
+ for (const patch of block.diff_block?.patches ?? []) {
1664
+ const path = patch.path ?? "";
1665
+ let value = patch.value ?? "";
1666
+ if (path === "/progress") continue;
1667
+ if (path.startsWith("/goals")) {
1668
+ if (typeof value === "object" && value?.chunks) {
1669
+ value = value.chunks.join("");
1670
+ }
1671
+ if (typeof value === "string" && value) {
1672
+ if (value.startsWith(fullReasoning)) {
1673
+ value = value.slice(fullReasoning.length);
1674
+ }
1675
+ fullReasoning += value;
1676
+ }
1677
+ continue;
1678
+ }
1679
+ if (field !== "markdown_block") continue;
1680
+ if (typeof value === "object" && value !== null) {
1681
+ if (Array.isArray(value.chunks) && value.chunks.length > 0) {
1682
+ value = value.chunks.join("");
1683
+ } else {
1684
+ value = value.answer ?? "";
1685
+ }
1686
+ }
1687
+ if (typeof value !== "string" || !value) continue;
1688
+ if (isSection) {
1689
+ const prev = sectionTexts.get(intended) ?? "";
1690
+ sectionTexts.set(intended, prev + value);
1691
+ } else if (isSummary) {
1692
+ summaryText += value;
1693
+ } else {
1694
+ if (value.startsWith(fullAnswer)) {
1695
+ value = value.slice(fullAnswer.length);
1696
+ } else if (fullAnswer.endsWith(value)) {
1697
+ value = "";
1698
+ }
1699
+ fullAnswer += value;
1700
+ }
1701
+ }
1702
+ }
1703
+ if (jsonData.text && !fullAnswer && sectionTexts.size === 0 && !summaryText) {
1704
+ try {
1705
+ const parsed = typeof jsonData.text === "string" ? JSON.parse(jsonData.text) : jsonData.text;
1706
+ if (Array.isArray(parsed)) {
1707
+ for (const step of parsed) {
1708
+ if (step.step_type === "FINAL" && step.content?.answer) {
1709
+ const answerObj = JSON.parse(step.content.answer);
1710
+ fullAnswer = answerObj.answer ?? "";
1711
+ break;
1712
+ }
1713
+ }
1714
+ }
1715
+ } catch {
1716
+ }
1717
+ }
1718
+ }
1719
+ if (sectionTexts.size > 1) {
1720
+ const sortedSections = [...sectionTexts.entries()].sort(([a], [b]) => {
1721
+ const numA = parseInt(a.match(/\d+/)?.[0] ?? "0");
1722
+ const numB = parseInt(b.match(/\d+/)?.[0] ?? "0");
1723
+ return numA - numB;
1724
+ }).map(([, text2]) => text2.trim()).filter((t) => t.length > 0);
1725
+ if (sortedSections.length > 0) {
1726
+ fullAnswer = sortedSections.join("\n\n");
1727
+ console.error(`[perplexity-mcp] Research report: ${sectionTexts.size} sections, ${fullAnswer.length} chars total.`);
1728
+ }
1729
+ } else if (sectionTexts.size === 1) {
1730
+ const singleSection = [...sectionTexts.values()][0].trim();
1731
+ if (singleSection.length > fullAnswer.length) {
1732
+ fullAnswer = singleSection;
1733
+ }
1734
+ }
1735
+ if (!fullAnswer && summaryText) {
1736
+ fullAnswer = summaryText.trim();
1737
+ }
1738
+ const result = {
1739
+ answer: fullAnswer.trim(),
1740
+ sources: webSources,
1741
+ media,
1742
+ suggestedFollowups: followups
1743
+ };
1744
+ if (fullReasoning) result.reasoning = fullReasoning.trim();
1745
+ if (backendUuid) {
1746
+ result.followUp = {
1747
+ backendUuid,
1748
+ readWriteToken,
1749
+ threadUrlSlug,
1750
+ threadTitle
1751
+ };
1752
+ }
1753
+ if (threadUrlSlug) {
1754
+ result.threadUrl = `${PERPLEXITY_URL}/search/${threadUrlSlug}`;
1755
+ }
1756
+ return result;
1757
+ }
1758
+ /**
1759
+ * Execute arbitrary JS in the browser context. Used for API inspection.
1760
+ */
1761
+ async evaluateInBrowser(fn, arg) {
1762
+ if (!this.page) throw new Error("Client not initialized");
1763
+ return this.page.evaluate(fn, arg);
1764
+ }
1765
+ /**
1766
+ * Navigate to a URL, intercept network requests for a duration, return matched ones.
1767
+ */
1768
+ async interceptRequests(url, durationMs, filter) {
1769
+ if (!this.page) throw new Error("Client not initialized");
1770
+ const captured = [];
1771
+ const handler = async (response) => {
1772
+ const reqUrl = response.url();
1773
+ if (filter(reqUrl)) {
1774
+ let body;
1775
+ try {
1776
+ body = await response.text();
1777
+ } catch {
1778
+ }
1779
+ captured.push({
1780
+ method: response.request().method(),
1781
+ url: reqUrl,
1782
+ status: response.status(),
1783
+ body: body?.slice(0, 2e3)
1784
+ });
1785
+ }
1786
+ };
1787
+ this.page.on("response", handler);
1788
+ await this.page.goto(url, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
1789
+ });
1790
+ await this.page.waitForTimeout(durationMs);
1791
+ this.page.off("response", handler);
1792
+ return captured;
1793
+ }
1794
+ async resolveThreadEntryUuid(threadSlug) {
1795
+ if (!this.page) throw new Error("Client not initialized");
1796
+ const rawJson = await this.page.evaluate(
1797
+ async (url) => {
1798
+ const response = await fetch(url, { credentials: "include" });
1799
+ return response.text();
1800
+ },
1801
+ THREAD_ENDPOINT(threadSlug)
1802
+ );
1803
+ const threadData = JSON.parse(rawJson);
1804
+ if (threadData?.status !== "success") {
1805
+ return null;
1806
+ }
1807
+ const entries = Array.isArray(threadData.entries) ? threadData.entries : [];
1808
+ const lastEntry = entries[entries.length - 1] ?? null;
1809
+ return lastEntry?.uuid ?? threadData?.last_entry_uuid ?? null;
1810
+ }
1811
+ async exportThread(opts) {
1812
+ if (!this.page) {
1813
+ const fast = await exportThreadViaImpit(opts);
1814
+ if (fast) return fast;
1815
+ }
1816
+ if (!this.page) throw new Error("Client not initialized");
1817
+ const entryUuid = opts.entryUuid ?? (opts.threadSlug ? await this.resolveThreadEntryUuid(opts.threadSlug) : null);
1818
+ if (!entryUuid) {
1819
+ throw new Error("Could not resolve the Perplexity entry UUID for export.");
1820
+ }
1821
+ return exportThread({
1822
+ entryUuid,
1823
+ format: opts.format,
1824
+ fetchImpl: async (url, init) => {
1825
+ const response = await this.page.evaluate(
1826
+ async ({ requestUrl, requestInit }) => {
1827
+ const resp = await fetch(requestUrl, {
1828
+ ...requestInit,
1829
+ credentials: "include"
1830
+ });
1831
+ const text = await resp.text();
1832
+ return {
1833
+ status: resp.status,
1834
+ headers: Object.fromEntries(resp.headers.entries()),
1835
+ text
1836
+ };
1837
+ },
1838
+ {
1839
+ requestUrl: url,
1840
+ requestInit: {
1841
+ method: init?.method,
1842
+ headers: init?.headers,
1843
+ body: typeof init?.body === "string" ? init.body : void 0
1844
+ }
1845
+ }
1846
+ );
1847
+ return new Response(response.text, {
1848
+ status: response.status,
1849
+ headers: response.headers
1850
+ });
1851
+ }
1852
+ });
1853
+ }
1854
+ /**
1855
+ * Authenticated fetch helper — routes through the persistent page so
1856
+ * cookies (session-token + cf_clearance) ride along. Mirrors the
1857
+ * exportThread() pattern but returns parsed JSON.
1858
+ */
1859
+ async pageFetchJson(url, init) {
1860
+ if (!this.page) throw new Error("Client not initialized");
1861
+ const payload = init?.body != null ? JSON.stringify(init.body) : null;
1862
+ return this.page.evaluate(
1863
+ async ({ u, method, body, headers }) => {
1864
+ const resp = await fetch(u, {
1865
+ method: method ?? "GET",
1866
+ credentials: "include",
1867
+ headers: { "content-type": "application/json", ...headers ?? {} },
1868
+ body
1869
+ });
1870
+ const ct = resp.headers.get("content-type") ?? "";
1871
+ const text = await resp.text();
1872
+ let data = text;
1873
+ if (ct.includes("application/json")) {
1874
+ try {
1875
+ data = JSON.parse(text);
1876
+ } catch {
1877
+ }
1878
+ }
1879
+ return { status: resp.status, data };
1880
+ },
1881
+ { u: url, method: init?.method ?? "GET", body: payload, headers: init?.headers ?? null }
1882
+ );
1883
+ }
1884
+ /**
1885
+ * Fetch a page of the user's library via POST /rest/thread/list_ask_threads.
1886
+ * Endpoint captured 2026-04-21 — body shape documented in
1887
+ * docs/export-endpoint-capture.md (alongside export).
1888
+ */
1889
+ async listCloudThreads(opts = {}) {
1890
+ const url = buildListAskThreadsUrl();
1891
+ const body = buildListAskThreadsBody(opts);
1892
+ if (!this.page && isImpitAvailable()) {
1893
+ const fast = await listCloudThreadsViaImpit(opts);
1894
+ if (fast) return fast;
1895
+ }
1896
+ if (!this.page) await this.init();
1897
+ const { status, data } = await this.pageFetchJson(url, { method: "POST", body });
1898
+ if (status === 403 || status === 401) {
1899
+ throw new Error(`Perplexity rejected list_ask_threads (status ${status}). Re-login and retry.`);
1900
+ }
1901
+ if (status !== 200 || !Array.isArray(data)) {
1902
+ throw new Error(`list_ask_threads failed: status ${status}`);
1903
+ }
1904
+ return parseListThreadsRows(data);
1905
+ }
1906
+ /**
1907
+ * Fetch full content of a single cloud thread by slug. Tries impit first,
1908
+ * falls back to the page-context fetch. Endpoint:
1909
+ * GET /rest/thread/<slug>?from_first=true (captured 2026-04-21).
1910
+ */
1911
+ async getCloudThread(slug, opts = {}) {
1912
+ if (!slug) throw new Error("getCloudThread: slug required");
1913
+ if (!this.page && isImpitAvailable()) {
1914
+ const fast = await getCloudThreadViaImpit(slug, opts);
1915
+ if (fast) return fast;
1916
+ }
1917
+ if (!this.page) await this.init();
1918
+ const url = buildGetCloudThreadUrl(slug, opts.limit ?? 50);
1919
+ const { status, data } = await this.pageFetchJson(url, { headers: getCloudThreadHeaders(url) });
1920
+ if (status === 404) throw new Error(`Thread '${slug}' not found on Perplexity (404).`);
1921
+ if (status !== 200 || typeof data !== "object" || data == null) {
1922
+ throw new Error(`getCloudThread failed: status ${status}`);
1923
+ }
1924
+ return parseCloudThreadResponse(slug, data);
1925
+ }
1926
+ async shutdown() {
1927
+ if (this.context) {
1928
+ await this.context.close().catch(() => {
1929
+ });
1930
+ this.context = null;
1931
+ this.page = null;
1932
+ }
1933
+ if (this.browser) {
1934
+ await this.browser.close().catch(() => {
1935
+ });
1936
+ this.browser = null;
1937
+ }
1938
+ }
1939
+ };
1940
+
1941
+ export {
1942
+ readCachedAccountInfoFromDisk,
1943
+ listCloudThreadsViaImpit,
1944
+ getCloudThreadViaImpit,
1945
+ isExperimentalImpitSearchEnabled,
1946
+ searchPerplexityViaImpit,
1947
+ retrieveThreadViaImpit,
1948
+ exportThreadViaImpit,
1949
+ PerplexityClient
1950
+ };