perplexity-user-mcp 0.8.37 → 0.8.39

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 (63) hide show
  1. package/README.md +203 -9
  2. package/dist/checks/vault.d.ts +41 -0
  3. package/dist/checks/vault.mjs +23 -0
  4. package/dist/{chunk-Q2VY4R5F.mjs → chunk-2FPGJKCA.mjs} +2 -2
  5. package/dist/{chunk-ZQFUZPLO.mjs → chunk-452DK6OS.mjs} +2 -2
  6. package/dist/{chunk-OF4DMAPJ.mjs → chunk-B65IJQZJ.mjs} +1 -1
  7. package/dist/{chunk-H4BUAPPO.mjs → chunk-C3HPFFTD.mjs} +4 -4
  8. package/dist/{chunk-LZPLNZ5U.mjs → chunk-D254EFYB.mjs} +1 -1
  9. package/dist/{chunk-Z7DAACGZ.mjs → chunk-DQQISMYN.mjs} +2 -2
  10. package/dist/{chunk-3B276PGG.mjs → chunk-FKQ3HP4Q.mjs} +1 -1
  11. package/dist/{chunk-7JL36EBH.mjs → chunk-HNSPNCFH.mjs} +1 -1
  12. package/dist/{chunk-6EP2BLTV.mjs → chunk-KJFX2ZXR.mjs} +1 -1
  13. package/dist/{chunk-SVPRB62V.mjs → chunk-NJX4RBO6.mjs} +1 -1
  14. package/dist/{chunk-X45O6YD3.mjs → chunk-RK4EBZJ3.mjs} +28 -9
  15. package/dist/{chunk-TQLCLE4L.mjs → chunk-S677V2JU.mjs} +57 -12
  16. package/dist/{chunk-S5VD7WTU.mjs → chunk-T6ARJK2P.mjs} +6 -6
  17. package/dist/{chunk-HTUAQRKH.mjs → chunk-TDXETAQT.mjs} +1 -1
  18. package/dist/{chunk-LKJMLGFP.mjs → chunk-U7QPUNRH.mjs} +2 -2
  19. package/dist/{chunk-PE23RMXY.mjs → chunk-V4U3JM4R.mjs} +1 -1
  20. package/dist/chunk-WDIW33DA.mjs +77 -0
  21. package/dist/{chunk-KCXM2M4B.mjs → chunk-XTRJSV72.mjs} +1 -1
  22. package/dist/cli.d.ts +348 -2
  23. package/dist/cli.mjs +259 -3
  24. package/dist/client.mjs +6 -6
  25. package/dist/cloud-sync.mjs +8 -8
  26. package/dist/config.mjs +3 -3
  27. package/dist/daemon/attach.mjs +17 -17
  28. package/dist/daemon/audit.mjs +2 -2
  29. package/dist/daemon/client-http.mjs +17 -17
  30. package/dist/daemon/index.mjs +18 -18
  31. package/dist/daemon/install-tunnel.mjs +2 -2
  32. package/dist/daemon/launcher.mjs +16 -16
  33. package/dist/daemon/lockfile.mjs +2 -2
  34. package/dist/daemon/server.mjs +11 -11
  35. package/dist/daemon/token.mjs +2 -2
  36. package/dist/daemon/tunnel-providers/index.mjs +3 -3
  37. package/dist/doctor.mjs +2 -2
  38. package/dist/export.mjs +4 -4
  39. package/dist/health-check.d.ts +1 -1
  40. package/dist/health-check.mjs +3 -3
  41. package/dist/history-store.mjs +2 -2
  42. package/dist/impit-login-runner.d.ts +1 -1
  43. package/dist/impit-login-runner.mjs +4 -4
  44. package/dist/index.d.ts +5 -1
  45. package/dist/index.mjs +96 -24
  46. package/dist/login-runner.d.ts +1 -1
  47. package/dist/login-runner.mjs +3 -3
  48. package/dist/logout.d.ts +1 -1
  49. package/dist/logout.mjs +2 -2
  50. package/dist/manual-login-runner.d.ts +1 -1
  51. package/dist/manual-login-runner.mjs +3 -3
  52. package/dist/{native-deps-YNKXITRY.mjs → native-deps-IE4B55EL.mjs} +4 -4
  53. package/dist/profiles.mjs +1 -1
  54. package/dist/refresh.mjs +4 -4
  55. package/dist/reinit-watcher.d.ts +12 -1
  56. package/dist/reinit-watcher.mjs +4 -2
  57. package/dist/vault.d-BSJWDLhp.d.ts +37 -0
  58. package/dist/vault.mjs +4 -2
  59. package/dist/viewers.mjs +1 -1
  60. package/package.json +1 -1
  61. package/dist/chunk-U3DGFLXZ.mjs +0 -43
  62. package/dist/vault.d-BtRSLZiM.d.ts +0 -8
  63. /package/dist/{chunk-XKSWCEGI.mjs → chunk-HJIXH6CL.mjs} +0 -0
package/README.md CHANGED
@@ -16,22 +16,34 @@ or run on demand with `npx`:
16
16
  npx perplexity-user-mcp
17
17
  ```
18
18
 
19
- The server speaks MCP over stdio, so normally you point your MCP client (Claude Desktop, Cursor, Windsurf, Cline, Claude Code, Amp, Codex CLI) at the binary rather than invoking it directly.
19
+ The server speaks MCP over stdio, so normally you point your MCP client (Claude Desktop, Cursor, Windsurf, Cline, Claude Code, Amp, Codex CLI, VS Code MCP, Visual Studio 2022, OpenCode, GitHub Copilot CLI, Factory Droid, Qwen Code, Gemini CLI, Kiro, Firebase Studio, …) at the binary rather than invoking it directly.
20
20
 
21
21
  ## First run & login
22
22
 
23
23
  Login is interactive — Perplexity emails a one-time code that you have to paste back into the prompt. The MCP `perplexity_login` tool only returns instructions, because MCP tool calls cannot display interactive prompts. You log in either through the CLI or, if you have the VS Code extension, through its dashboard.
24
24
 
25
- Quick start from a fresh machine:
25
+ Pick the row that matches your environment for a copy-pasteable quick start. Everything below assumes the package is installed (`npm install -g perplexity-user-mcp`).
26
+
27
+ | Environment | Quick start |
28
+ |---|---|
29
+ | **A. Desktop + VS Code extension** | Install [the extension](https://marketplace.visualstudio.com/items?itemName=Nskha.perplexity-vscode), open the dashboard, click **Login**. The extension owns the browser, vault, and OTP prompt — no terminal needed. |
30
+ | **B. Desktop, standalone CLI (Win / Mac / Linux with display)** | `npx perplexity-user-mcp login --mode manual` — opens a visible browser, sign in with email / Google / GitHub / Apple SSO, runner persists cookies and exits. |
31
+ | **C. Desktop, standalone CLI, prefer terminal-only** | `npx perplexity-user-mcp install-speed-boost && npx perplexity-user-mcp login --mode auto --email me@example.com` — OTP prompt appears on stderr, paste the six-digit code from your email. |
32
+ | **D. Headless VPS, can receive your email** | First run `npx perplexity-user-mcp setup-vault` — if the box has no OS keychain (libsecret missing on Linux servers) it generates a passphrase and prints persistence snippets (PowerShell / setx / zsh / bash / systemd / MCP-client env block). Then same as **C**. Speed Boost (impit) does the email/OTP flow over HTTP with no browser. Falls back to a browser runner only on `cf_blocked`, which fails on a true headless box — see pattern **E** if that happens. |
33
+ | **E. Headless VPS, can't run a browser** | Log in on a desktop machine, then either set `PERPLEXITY_SESSION_TOKEN` from the cookie value on the VPS, **or** copy the vault. See [Headless / VPS deployment](#headless--vps-deployment) below. |
34
+ | **F. Headless VPS + a desktop you control** | Run `npx perplexity-user-mcp daemon start --tunnel` on the desktop; point the VPS's MCP client at the printed Cloudflare URL with the bearer token. The desktop owns the browser; the VPS only sees a bearer-authed HTTP MCP endpoint. |
35
+
36
+ Common verifications after any path:
26
37
 
27
38
  ```bash
28
- npm install -g perplexity-user-mcp
29
- npx perplexity-user-mcp install-speed-boost # optional, strongly recommended
30
- npx perplexity-user-mcp login --mode auto --email me@example.com
31
- # terminal will prompt for the OTP from your email
39
+ npx perplexity-user-mcp status # expect: valid, with a tier (Pro / Max / Enterprise / Authenticated)
40
+ npx perplexity-user-mcp doctor # green across the board, especially profiles + vault
32
41
  ```
33
42
 
34
- `--mode auto` runs the email + OTP flow over HTTP (impit-backed if Speed Boost is installed; otherwise driven through the browser). `--mode manual` opens a visible browser window so you can sign in with Google, GitHub, or Apple SSO.
43
+ What success vs failure looks like on the CLI:
44
+
45
+ - `login finished (0)` and `status` reports `valid` → you're done.
46
+ - Non-zero exit code from `login` is a failure even if the CLI prints a "finished" line. The runner emits a JSON line on stdout with the actual `reason`: `cf_blocked`, `chrome_missing`, `otp_rejected`, `crash`, `timeout`. Read stderr for the full message — that's where the structured error surfaces.
35
47
 
36
48
  Session state lives at `~/.perplexity-mcp/` (cookies, profile, models cache). Delete that directory to start over, or use `npx perplexity-user-mcp logout --purge`.
37
49
 
@@ -99,7 +111,7 @@ All overrides are optional and evaluated at call time:
99
111
  Most-used commands. Run `npx perplexity-user-mcp --help` for the full list (daemon, tunnel providers, etc.).
100
112
 
101
113
  ```bash
102
- npx perplexity-user-mcp # start MCP stdio server
114
+ npx perplexity-user-mcp # start MCP stdio server (no output until a client connects)
103
115
  npx perplexity-user-mcp login [--profile X] [--mode auto|manual] [--plain-cookies]
104
116
  npx perplexity-user-mcp logout [--profile X] [--purge]
105
117
  npx perplexity-user-mcp status [--profile X] [--all]
@@ -118,9 +130,71 @@ npx perplexity-user-mcp daemon start [--port N] [--tunnel]
118
130
  npx perplexity-user-mcp --version
119
131
  ```
120
132
 
133
+ > **Note** — `npx perplexity-user-mcp` with no subcommand starts the stdio MCP server and waits silently for JSON-RPC on stdin. There's no progress output; if you typed it expecting a login or status prompt, that's why "nothing happened." Use the explicit subcommand (`login`, `status`, etc.) for interactive operation.
134
+
135
+ ## Multiple accounts / profiles
136
+
137
+ Each Perplexity account lives in its own profile under `~/.perplexity-mcp/profiles/<name>/`. The active profile is the one tools read cookies from. Naming them after the plan tier (`pro`, `max`, `personal`, `work`) keeps things obvious.
138
+
139
+ ```bash
140
+ npx perplexity-user-mcp add-account --name pro --mode auto --email pro-account@example.com
141
+ # ...follow the OTP prompt, persist cookies under profiles/pro/
142
+
143
+ npx perplexity-user-mcp add-account --name personal --mode manual
144
+ # opens a browser, persist under profiles/personal/
145
+
146
+ npx perplexity-user-mcp list-accounts
147
+ # * pro [Pro] mode=auto lastLogin=2026-05-04...
148
+ # personal [Free] mode=manual lastLogin=2026-05-04...
149
+
150
+ npx perplexity-user-mcp switch-account personal # switch active profile
151
+ npx perplexity-user-mcp status # confirm "valid" for the new active profile
152
+ ```
153
+
154
+ The MCP server picks up the active profile change immediately — version 0.8.40+ watches the active-pointer file and reloads cookies on switch, so you don't need to restart the server (or your IDE) when toggling between accounts. Pre-0.8.40 you'd see the old profile's data until the daemon was restarted.
155
+
156
+ If you set `PERPLEXITY_PROFILE=<name>` in an MCP client's env block, that pins the server to that one profile regardless of `switch-account` — useful when you want one IDE on `pro` and another on `personal` simultaneously.
157
+
158
+ ## Headless / VPS deployment
159
+
160
+ `login --mode manual` and the browser fallback for `--mode auto` both launch a real Chromium and need a graphical session. On a true headless box (no X server, no DISPLAY, no Wayland) those paths fail at browser launch with a `chrome_missing` or `crash` reason. Three workable patterns:
161
+
162
+ **1. Terminal-only login (preferred when impit succeeds).** Try this first — it's the simplest and works on most VPS boxes if Cloudflare doesn't gate the email endpoint:
163
+
164
+ ```bash
165
+ npx perplexity-user-mcp install-speed-boost # required: provides the impit HTTP runner
166
+ npx perplexity-user-mcp login --mode auto --email me@example.com
167
+ # server emails the OTP; paste the six-digit code at the prompt on stderr
168
+ ```
169
+
170
+ If this fails with `cf_blocked`, fall back to one of the other patterns. (impit is opt-in; without it the auto runner falls back to the browser, which won't work headless.)
171
+
172
+ **2. Pre-supplied session token.** Log in on a desktop machine via any browser, extract the `__Secure-next-auth.session-token` cookie value, and feed it to the headless server:
173
+
174
+ ```bash
175
+ PERPLEXITY_SESSION_TOKEN=<long-jwt-from-the-cookie> \
176
+ PERPLEXITY_CSRF_TOKEN=<optional-companion-cookie> \
177
+ npx perplexity-user-mcp # stdio server bypasses login entirely
178
+ ```
179
+
180
+ The cookie expires when Perplexity rotates it (typically ~30 days); refresh by re-extracting from the desktop browser. This path is acceptable for personal-use VPS boxes where the env block is `chmod 600`; do not use on shared hosts.
181
+
182
+ **3. Daemon + tunnel from a desktop.** Run the daemon on a desktop machine you control, expose it via Cloudflare Quick Tunnels (built-in) or ngrok, and point your headless clients at the tunnel URL:
183
+
184
+ ```bash
185
+ # on the desktop:
186
+ npx perplexity-user-mcp daemon start --tunnel
187
+ # prints: tunnel URL https://<random>.trycloudflare.com bearer <token>
188
+ # on the VPS, point your MCP client at the tunnel URL with the bearer in Authorization
189
+ ```
190
+
191
+ The desktop owns the browser session and the vault; the VPS only sees a bearer-authed HTTP MCP endpoint. See [the Codex CLI setup guide](https://github.com/Automations-Project/VSCode-Perplexity-MCP/blob/main/docs/codex-cli-setup.md) for an end-to-end walkthrough using the same daemon + tunnel pattern.
192
+
193
+ **Why not just `login --mode manual` on the VPS?** It launches a headed Chromium that needs `$DISPLAY`. On a server distro you'd see `Failed to launch browser process` and the runner exits with `crash`. A virtual framebuffer (`xvfb-run`) would technically work but the email/OTP step still requires a way to interact with the email — pattern 1 covers that without the X11 dependency.
194
+
121
195
  ## MCP client configuration
122
196
 
123
- Example `mcp.json` entry (Cursor, Windsurf, Claude Code format):
197
+ Example `mcp.json` entry (Cursor, Windsurf, Claude Code, Cline, Amp, Kiro, Firebase Studio, Antigravity, Gemini CLI, Factory Droid, Qwen Code, Copilot CLI — `mcpServers` root key):
124
198
 
125
199
  ```json
126
200
  {
@@ -135,6 +209,40 @@ Example `mcp.json` entry (Cursor, Windsurf, Claude Code format):
135
209
 
136
210
  Claude Desktop (`claude_desktop_config.json`) uses the same shape.
137
211
 
212
+ **VS Code MCP / Visual Studio 2022** use the `servers` root key with a `type` discriminator:
213
+
214
+ ```json
215
+ {
216
+ "servers": {
217
+ "perplexity": {
218
+ "type": "stdio",
219
+ "command": "npx",
220
+ "args": ["-y", "perplexity-user-mcp"]
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ Workspace paths: `.vscode/mcp.json` (VS Code), `<sln>/.mcp.json` or `%USERPROFILE%\.mcp.json` (Visual Studio 2022).
227
+
228
+ **OpenCode** uses the `mcp` root key with a local-server entry shape:
229
+
230
+ ```json
231
+ {
232
+ "mcp": {
233
+ "perplexity": {
234
+ "type": "local",
235
+ "command": ["npx", "-y", "perplexity-user-mcp"],
236
+ "enabled": true
237
+ }
238
+ }
239
+ }
240
+ ```
241
+
242
+ Path: `~/.config/opencode/opencode.json`.
243
+
244
+ **Zed** uses the `context_servers` root key. **Codex CLI** uses TOML at `~/.codex/config.toml`.
245
+
138
246
  ## Environment variables
139
247
 
140
248
  | Variable | Purpose |
@@ -165,6 +273,92 @@ Claude Desktop (`claude_desktop_config.json`) uses the same shape.
165
273
  - `perplexity_login` — returns login instructions (interactive login runs via the CLI / extension)
166
274
  - `perplexity_doctor` — run diagnostic checks across browser, profile, auth, and network and return a Markdown report (pass `probe:true` for a live search probe)
167
275
 
276
+ ## Search sources and advanced queries
277
+
278
+ Search-style tools support Perplexity source focus through a `sources` argument:
279
+
280
+ - `web` — general web search. This is the default.
281
+ - `scholar` — scholarly / academic source focus.
282
+ - `social` — social discussion source focus.
283
+
284
+ The source selector is explicit. If `sources` is omitted, the server sends `["web"]`.
285
+
286
+ Examples:
287
+
288
+ ```json
289
+ {
290
+ "tool": "perplexity_search",
291
+ "arguments": {
292
+ "query": "recent papers on retrieval augmented generation evaluation",
293
+ "sources": ["scholar"],
294
+ "language": "en-US"
295
+ }
296
+ }
297
+ ```
298
+
299
+ ```json
300
+ {
301
+ "tool": "perplexity_ask",
302
+ "arguments": {
303
+ "query": "What are practitioners saying about Cursor versus Windsurf for large TypeScript repos?",
304
+ "sources": ["social"],
305
+ "mode": "copilot"
306
+ }
307
+ }
308
+ ```
309
+
310
+ ```json
311
+ {
312
+ "tool": "perplexity_research",
313
+ "arguments": {
314
+ "query": "Compare academic evidence and practitioner discussion around code review automation",
315
+ "sources": ["scholar", "social"],
316
+ "language": "en-US"
317
+ }
318
+ }
319
+ ```
320
+
321
+ Natural-language prompts usually work too when they name the desired source mode:
322
+
323
+ - "Use Perplexity scholar sources for recent papers on agentic search evaluation."
324
+ - "Search social sources for developer reports about Claude Code memory issues."
325
+ - "Run deep research using both scholar and web sources, and cite every claim."
326
+ - "Use `perplexity_ask` with `sources: [\"social\"]` and keep the answer concise."
327
+
328
+ Useful shorthand:
329
+
330
+ - "search ..." usually maps to `perplexity_search` for quick lookup and source discovery.
331
+ - "ask Perplexity ..." usually maps to `perplexity_ask` for a synthesized answer with citations.
332
+ - "reason through ..." usually maps to `perplexity_reason` for multi-step analysis.
333
+ - "research deeply ..." usually maps to `perplexity_research` for longer reports.
334
+ - "use ASI", "Computer mode", "run a compute task", or "do code/execution-style analysis" maps to `perplexity_compute` when the account has Computer-mode access.
335
+
336
+ For ASI / Computer mode, ask for `perplexity_compute` by name when precision matters:
337
+
338
+ ```json
339
+ {
340
+ "tool": "perplexity_compute",
341
+ "arguments": {
342
+ "query": "Model the true cost of a 5 kW residential solar installation in the Philippines versus investing the same cash at 6% annually over 10 and 20 years. Show assumptions, calculations, and sensitivity cases.",
343
+ "language": "en-US"
344
+ }
345
+ }
346
+ ```
347
+
348
+ ### Defaults
349
+
350
+ | Tool | Model default | Mode default | Sources default | Language default |
351
+ |---|---|---|---|---|
352
+ | `perplexity_search` | Authenticated: `pplx_pro`; anonymous: `turbo` | Authenticated: `copilot`; anonymous: `concise` | `["web"]` | `en-US` |
353
+ | `perplexity_ask` | `PERPLEXITY_SEARCH_MODEL` or `pplx_pro` | `copilot` | `["web"]` | `en-US` |
354
+ | `perplexity_reason` | `PERPLEXITY_REASON_MODEL` or `claude46sonnetthinking` | `copilot` | `["web"]` | `en-US` |
355
+ | `perplexity_research` | `PERPLEXITY_RESEARCH_MODEL` or `pplx_alpha` | `copilot` | `["web"]` | `en-US` |
356
+ | `perplexity_compute` | Tool argument, then `PERPLEXITY_COMPUTE_MODEL`, then account ASI default, then `pplx_asi` | `asi` | web-only Computer mode | `en-US` |
357
+
358
+ Model defaults are configurable with `PERPLEXITY_SEARCH_MODEL`, `PERPLEXITY_REASON_MODEL`, `PERPLEXITY_RESEARCH_MODEL`, and `PERPLEXITY_COMPUTE_MODEL`. `perplexity_ask`, `perplexity_reason`, and `perplexity_compute` also accept a per-call `model` argument. `perplexity_ask` accepts `mode: "concise" | "copilot"`.
359
+
360
+ Under the hood, search-style tools post a Perplexity web-app style request from the logged-in browser session to `https://www.perplexity.ai/rest/sse/perplexity_ask`. The response is a Server-Sent Events stream, which the MCP runtime parses into answer text, citation sources, media items, suggested follow-ups, follow-up context, and the Perplexity thread URL.
361
+
168
362
  ## Library use
169
363
 
170
364
  Subpath exports are published for embedding the same runtime inside other Node tooling:
@@ -95,6 +95,47 @@ async function run(opts = {}) {
95
95
  }
96
96
  }
97
97
 
98
+ // Active-decrypt verification — only when an encrypted vault.enc actually
99
+ // exists. This catches the "user has both keychain + passphrase set, but
100
+ // vault.enc was written with one and the read path now prefers the other"
101
+ // failure mode that surfaces as "Vault decrypt failed: wrong passphrase
102
+ // or corrupted ciphertext" mid-login. A status check that just reports
103
+ // "OS keychain holds master key" is misleading if the key can't actually
104
+ // open the on-disk blob.
105
+ if (existsSync(enc) && (kc.hasKey || envPass)) {
106
+ try {
107
+ const { Vault, __resetKeyCache } = await import('../vault.d-BSJWDLhp.d.ts');
108
+ // Use a fresh resolution context so the doctor's verification doesn't
109
+ // pollute the cached unseal material for the rest of the process.
110
+ __resetKeyCache();
111
+ // Vault.get returns null for absent keys without throwing; only a
112
+ // genuine decrypt failure throws.
113
+ await new Vault().get(profile, "cookies");
114
+ results.push({
115
+ category: CATEGORY,
116
+ name: "unseal-verify",
117
+ status: "pass",
118
+ message: "vault.enc decrypts cleanly with the active unseal material",
119
+ });
120
+ } catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ const isDecryptFailure = /wrong passphrase or corrupted ciphertext|Vault decrypt failed/.test(msg);
123
+ results.push({
124
+ category: CATEGORY,
125
+ name: "unseal-verify",
126
+ status: "fail",
127
+ message: isDecryptFailure
128
+ ? "vault.enc cannot be decrypted with any available unseal material"
129
+ : `vault.enc unreadable: ${msg}`,
130
+ hint: isDecryptFailure
131
+ ? (kc.hasKey && envPass
132
+ ? "Both keychain and PERPLEXITY_VAULT_PASSPHRASE are set, but neither matches the blob. The blob was likely written under a since-rotated passphrase or a different keychain key. Run 'perplexity-user-mcp logout --purge' on this profile and log in again to write a fresh vault."
133
+ : "The unseal material has changed since this blob was written. Restore the original passphrase, or run 'perplexity-user-mcp logout --purge' on this profile and log in again.")
134
+ : "Inspect the file at the path under 'profiles' check; consider restoring from backup or purging.",
135
+ });
136
+ }
137
+ }
138
+
98
139
  return results;
99
140
  }
100
141
 
@@ -83,6 +83,29 @@ async function run(opts = {}) {
83
83
  });
84
84
  }
85
85
  }
86
+ if (existsSync(enc) && (kc.hasKey || envPass)) {
87
+ try {
88
+ const { Vault, __resetKeyCache } = await import("../vault.mjs");
89
+ __resetKeyCache();
90
+ await new Vault().get(profile, "cookies");
91
+ results.push({
92
+ category: CATEGORY,
93
+ name: "unseal-verify",
94
+ status: "pass",
95
+ message: "vault.enc decrypts cleanly with the active unseal material"
96
+ });
97
+ } catch (err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ const isDecryptFailure = /wrong passphrase or corrupted ciphertext|Vault decrypt failed/.test(msg);
100
+ results.push({
101
+ category: CATEGORY,
102
+ name: "unseal-verify",
103
+ status: "fail",
104
+ message: isDecryptFailure ? "vault.enc cannot be decrypted with any available unseal material" : `vault.enc unreadable: ${msg}`,
105
+ hint: isDecryptFailure ? kc.hasKey && envPass ? "Both keychain and PERPLEXITY_VAULT_PASSPHRASE are set, but neither matches the blob. The blob was likely written under a since-rotated passphrase or a different keychain key. Run 'perplexity-user-mcp logout --purge' on this profile and log in again to write a fresh vault." : "The unseal material has changed since this blob was written. Restore the original passphrase, or run 'perplexity-user-mcp logout --purge' on this profile and log in again." : "Inspect the file at the path under 'profiles' check; consider restoring from backup or purging."
106
+ });
107
+ }
108
+ }
86
109
  return results;
87
110
  }
88
111
  export {
@@ -2,11 +2,11 @@ import {
2
2
  PerplexityClient,
3
3
  getCloudThreadViaImpit,
4
4
  listCloudThreadsViaImpit
5
- } from "./chunk-H4BUAPPO.mjs";
5
+ } from "./chunk-C3HPFFTD.mjs";
6
6
  import {
7
7
  hydrateCloudEntry,
8
8
  upsertFromCloud
9
- } from "./chunk-OF4DMAPJ.mjs";
9
+ } from "./chunk-B65IJQZJ.mjs";
10
10
 
11
11
  // src/cloud-sync.js
12
12
  var DEFAULT_PAGE_SIZE = 1e3;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  ensureDaemon
3
- } from "./chunk-X45O6YD3.mjs";
3
+ } from "./chunk-RK4EBZJ3.mjs";
4
4
  import {
5
5
  getPackageVersion
6
- } from "./chunk-S5VD7WTU.mjs";
6
+ } from "./chunk-T6ARJK2P.mjs";
7
7
 
8
8
  // src/daemon/client-http.ts
9
9
  import { randomUUID } from "crypto";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getActiveName,
3
3
  getProfilePaths
4
- } from "./chunk-XKSWCEGI.mjs";
4
+ } from "./chunk-HJIXH6CL.mjs";
5
5
 
6
6
  // src/history-store.js
7
7
  import { randomUUID } from "crypto";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  impitFetchJson,
3
3
  isImpitAvailable
4
- } from "./chunk-Z7DAACGZ.mjs";
4
+ } from "./chunk-DQQISMYN.mjs";
5
5
  import {
6
6
  FORMAT_TO_CONTENT_TYPE,
7
7
  exportThread,
8
8
  resolveExportApiFormat
9
- } from "./chunk-LZPLNZ5U.mjs";
9
+ } from "./chunk-D254EFYB.mjs";
10
10
  import {
11
11
  ASI_ACCESS_ENDPOINT,
12
12
  AUTH_SESSION_ENDPOINT,
@@ -21,12 +21,12 @@ import {
21
21
  getOrCreateContext,
22
22
  getSavedCookies,
23
23
  resolveBrowserExecutable
24
- } from "./chunk-LKJMLGFP.mjs";
24
+ } from "./chunk-U7QPUNRH.mjs";
25
25
  import {
26
26
  getActiveName,
27
27
  getConfigDir,
28
28
  getProfilePaths
29
- } from "./chunk-XKSWCEGI.mjs";
29
+ } from "./chunk-HJIXH6CL.mjs";
30
30
 
31
31
  // src/client.ts
32
32
  import { randomUUID } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PERPLEXITY_URL
3
- } from "./chunk-LKJMLGFP.mjs";
3
+ } from "./chunk-U7QPUNRH.mjs";
4
4
 
5
5
  // src/export.js
6
6
  import { Buffer } from "buffer";
@@ -9,12 +9,12 @@ import {
9
9
  getOrCreateContext,
10
10
  getSavedCookies,
11
11
  resolveBrowserExecutable
12
- } from "./chunk-LKJMLGFP.mjs";
12
+ } from "./chunk-U7QPUNRH.mjs";
13
13
  import {
14
14
  getActiveName,
15
15
  getConfigDir,
16
16
  getProfilePaths
17
- } from "./chunk-XKSWCEGI.mjs";
17
+ } from "./chunk-HJIXH6CL.mjs";
18
18
 
19
19
  // src/refresh.ts
20
20
  import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getConfigDir
3
- } from "./chunk-XKSWCEGI.mjs";
3
+ } from "./chunk-HJIXH6CL.mjs";
4
4
 
5
5
  // src/daemon/install-tunnel.ts
6
6
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getConfigDir
3
- } from "./chunk-XKSWCEGI.mjs";
3
+ } from "./chunk-HJIXH6CL.mjs";
4
4
  import {
5
5
  __glob
6
6
  } from "./chunk-4UEJOM6W.mjs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getConfigDir
3
- } from "./chunk-XKSWCEGI.mjs";
3
+ } from "./chunk-HJIXH6CL.mjs";
4
4
 
5
5
  // src/daemon/lockfile.ts
6
6
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ensureDaemon
3
- } from "./chunk-X45O6YD3.mjs";
3
+ } from "./chunk-RK4EBZJ3.mjs";
4
4
 
5
5
  // src/daemon/attach.ts
6
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getTunnelProvider,
3
3
  readTunnelSettings
4
- } from "./chunk-KCXM2M4B.mjs";
4
+ } from "./chunk-XTRJSV72.mjs";
5
5
  import {
6
6
  acquire,
7
7
  getLockfilePath,
@@ -9,26 +9,27 @@ import {
9
9
  read,
10
10
  release,
11
11
  replace
12
- } from "./chunk-6EP2BLTV.mjs";
12
+ } from "./chunk-KJFX2ZXR.mjs";
13
13
  import {
14
14
  getPackageVersion,
15
15
  startDaemonServer
16
- } from "./chunk-S5VD7WTU.mjs";
16
+ } from "./chunk-T6ARJK2P.mjs";
17
17
  import {
18
18
  ensureToken,
19
19
  getTokenPath,
20
20
  readToken
21
- } from "./chunk-HTUAQRKH.mjs";
21
+ } from "./chunk-TDXETAQT.mjs";
22
22
  import {
23
+ watchActiveProfile,
23
24
  watchReinit
24
- } from "./chunk-U3DGFLXZ.mjs";
25
+ } from "./chunk-WDIW33DA.mjs";
25
26
  import {
26
27
  PerplexityClient
27
- } from "./chunk-H4BUAPPO.mjs";
28
+ } from "./chunk-C3HPFFTD.mjs";
28
29
  import {
29
30
  getActiveName,
30
31
  getConfigDir
31
- } from "./chunk-XKSWCEGI.mjs";
32
+ } from "./chunk-HJIXH6CL.mjs";
32
33
 
33
34
  // src/daemon/launcher.ts
34
35
  import { spawn } from "child_process";
@@ -186,13 +187,15 @@ async function startDaemon(options = {}) {
186
187
  continue;
187
188
  }
188
189
  let watcher;
190
+ let activeWatcher;
189
191
  let server;
190
192
  let finalizePromise = null;
191
193
  let finalizeResolve;
192
194
  const closed = new Promise((resolve) => {
193
195
  finalizeResolve = resolve;
194
196
  });
195
- const profile = process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
197
+ let currentWatchedProfile = process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
198
+ const profile = currentWatchedProfile;
196
199
  const client = options.createClient ? options.createClient() : new PerplexityClient();
197
200
  let tunnelState = {
198
201
  status: "disabled",
@@ -289,6 +292,7 @@ async function startDaemon(options = {}) {
289
292
  finalizePromise = (async () => {
290
293
  await disableTunnelRuntime().catch(() => void 0);
291
294
  watcher?.dispose();
295
+ activeWatcher?.dispose();
292
296
  if (options.signal && abortHandler) {
293
297
  options.signal.removeEventListener("abort", abortHandler);
294
298
  }
@@ -313,9 +317,24 @@ async function startDaemon(options = {}) {
313
317
  await finalize();
314
318
  };
315
319
  try {
316
- watcher = watchReinit(profile, async () => {
320
+ watcher = watchReinit(currentWatchedProfile, async () => {
317
321
  await client.reinit();
318
322
  });
323
+ activeWatcher = watchActiveProfile(configDir, async () => {
324
+ try {
325
+ const nextProfile = process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
326
+ if (nextProfile !== currentWatchedProfile) {
327
+ currentWatchedProfile = nextProfile;
328
+ watcher?.dispose();
329
+ watcher = watchReinit(nextProfile, async () => {
330
+ await client.reinit();
331
+ });
332
+ }
333
+ await client.reinit();
334
+ } catch (err) {
335
+ console.error(`[perplexity-mcp] active-profile watcher: ${err.message}`);
336
+ }
337
+ });
319
338
  server = await startDaemonServer({
320
339
  host: options.host,
321
340
  port: options.port,