perplexity-user-mcp 0.8.38 → 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 (62) hide show
  1. package/README.md +117 -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-YUGDOXIN.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.mjs +57 -22
  45. package/dist/login-runner.d.ts +1 -1
  46. package/dist/login-runner.mjs +3 -3
  47. package/dist/logout.d.ts +1 -1
  48. package/dist/logout.mjs +2 -2
  49. package/dist/manual-login-runner.d.ts +1 -1
  50. package/dist/manual-login-runner.mjs +3 -3
  51. package/dist/{native-deps-YNKXITRY.mjs → native-deps-IE4B55EL.mjs} +4 -4
  52. package/dist/profiles.mjs +1 -1
  53. package/dist/refresh.mjs +4 -4
  54. package/dist/reinit-watcher.d.ts +12 -1
  55. package/dist/reinit-watcher.mjs +4 -2
  56. package/dist/vault.d-BSJWDLhp.d.ts +37 -0
  57. package/dist/vault.mjs +4 -2
  58. package/dist/viewers.mjs +1 -1
  59. package/package.json +1 -1
  60. package/dist/chunk-U3DGFLXZ.mjs +0 -43
  61. package/dist/vault.d-BtRSLZiM.d.ts +0 -8
  62. /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 |
@@ -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,
@@ -3,12 +3,12 @@ import {
3
3
  } from "./chunk-MTDFKNXX.mjs";
4
4
  import {
5
5
  getProfilePaths
6
- } from "./chunk-XKSWCEGI.mjs";
6
+ } from "./chunk-HJIXH6CL.mjs";
7
7
 
8
8
  // src/vault.js
9
9
  import { createCipheriv, createDecipheriv, randomBytes, hkdfSync, scrypt as nodeScrypt } from "crypto";
10
10
  import { promisify } from "util";
11
- import { existsSync, readFileSync, mkdirSync, rmSync } from "fs";
11
+ import { existsSync, readFileSync, mkdirSync, renameSync, rmSync } from "fs";
12
12
  import { dirname } from "path";
13
13
  var MAGIC = Buffer.from("PXVT");
14
14
  var VERSION_V1 = 1;
@@ -255,6 +255,14 @@ async function getUnsealMaterial() {
255
255
  "Vault locked: no keychain, no env var, no TTY. Three unseal paths on Linux/headless: (a) install an OS keychain (libsecret + gnome-keyring) so the MCP process can read it, (b) set PERPLEXITY_VAULT_PASSPHRASE in your IDE's MCP server env block, or (c) run the VS Code extension's daemon and connect over HTTP transport instead of stdio. Codex CLI setup: docs/codex-cli-setup.md. Generic vault-unseal docs: docs/vault-unseal.md."
256
256
  );
257
257
  }
258
+ async function getAllUnsealMaterials() {
259
+ const materials = [];
260
+ const fromKc = await keyFromKeychain();
261
+ if (fromKc) materials.push({ kind: "key", key: fromKc });
262
+ const envPass = process.env.PERPLEXITY_VAULT_PASSPHRASE;
263
+ if (envPass) materials.push({ kind: "passphrase", passphrase: envPass });
264
+ return materials;
265
+ }
258
266
  async function getMasterKey() {
259
267
  if (_keyCache) return _keyCache;
260
268
  const unseal = await getUnsealMaterial();
@@ -280,16 +288,31 @@ async function readVaultObject(profileName) {
280
288
  if (!existsSync(p)) return {};
281
289
  const blob = readFileSync(p);
282
290
  const header = parseVaultHeader(blob);
283
- const unseal = await getUnsealMaterial();
284
- const key = await deriveKeyForHeader(header, unseal);
285
- const plain = aesGcmOpen(header, key);
286
- try {
287
- return JSON.parse(plain.toString("utf8"));
288
- } catch (err) {
289
- const { redact } = await import("./redact.mjs");
290
- console.error(`[vault] Corrupt vault JSON for profile ${redact(profileName)}: ${redact(err.message)}`);
291
- throw new Error(`Vault for profile '${profileName}' is corrupt or unreadable.`);
291
+ const materials = _unsealMaterialCache ? [_unsealMaterialCache, ...(await getAllUnsealMaterials()).filter((m) => m !== _unsealMaterialCache)] : await getAllUnsealMaterials();
292
+ if (materials.length === 0) {
293
+ await getUnsealMaterial();
294
+ return {};
292
295
  }
296
+ let lastErr;
297
+ for (const unseal of materials) {
298
+ try {
299
+ const key = await deriveKeyForHeader(header, unseal);
300
+ const plain = aesGcmOpen(header, key);
301
+ try {
302
+ const parsed = JSON.parse(plain.toString("utf8"));
303
+ _unsealMaterialCache = unseal;
304
+ return parsed;
305
+ } catch (err) {
306
+ const { redact } = await import("./redact.mjs");
307
+ console.error(`[vault] Corrupt vault JSON for profile ${redact(profileName)}: ${redact(err.message)}`);
308
+ throw new Error(`Vault for profile '${profileName}' is corrupt or unreadable.`);
309
+ }
310
+ } catch (err) {
311
+ lastErr = err;
312
+ if (!/wrong passphrase or corrupted ciphertext/.test(String(err?.message ?? ""))) throw err;
313
+ }
314
+ }
315
+ throw lastErr ?? new Error("Vault decrypt failed: no unseal material succeeded.");
293
316
  }
294
317
  async function writeVaultObject(profileName, obj) {
295
318
  const paths = getProfilePaths(profileName);
@@ -319,7 +342,28 @@ var Vault = class {
319
342
  return obj[key] ?? null;
320
343
  }
321
344
  async set(profile, key, value) {
322
- const obj = await readVaultObject(profile);
345
+ let obj;
346
+ try {
347
+ obj = await readVaultObject(profile);
348
+ } catch (err) {
349
+ const msg = String(err?.message ?? "");
350
+ if (/wrong passphrase or corrupted ciphertext|Vault decrypt failed/.test(msg)) {
351
+ const paths = getProfilePaths(profile);
352
+ if (existsSync(paths.vault)) {
353
+ const quarantine = `${paths.vault}.unreadable.${Date.now()}.bak`;
354
+ try {
355
+ renameSync(paths.vault, quarantine);
356
+ } catch {
357
+ }
358
+ console.error(
359
+ `[vault] WARN existing vault.enc for profile '${profile}' could not be decrypted with any available unseal material; quarantined at ${quarantine} and starting fresh. Possible cause: keychain key rotation or PERPLEXITY_VAULT_PASSPHRASE change. Original error: ${msg}`
360
+ );
361
+ }
362
+ obj = {};
363
+ } else {
364
+ throw err;
365
+ }
366
+ }
323
367
  obj[key] = value;
324
368
  await writeVaultObject(profile, obj);
325
369
  }
@@ -340,6 +384,7 @@ export {
340
384
  decryptBlob,
341
385
  __resetKeyCache,
342
386
  getUnsealMaterial,
387
+ getAllUnsealMaterials,
343
388
  getMasterKey,
344
389
  Vault
345
390
  };
@@ -2,22 +2,22 @@ import {
2
2
  appendAuditEntry,
3
3
  getAuditLogPath,
4
4
  readAuditTail
5
- } from "./chunk-PE23RMXY.mjs";
5
+ } from "./chunk-V4U3JM4R.mjs";
6
6
  import {
7
7
  ensureToken,
8
8
  getTokenPath,
9
9
  rotateToken
10
- } from "./chunk-HTUAQRKH.mjs";
10
+ } from "./chunk-TDXETAQT.mjs";
11
11
  import {
12
12
  hydrateCloudHistoryEntry,
13
13
  syncCloudHistory
14
- } from "./chunk-Q2VY4R5F.mjs";
14
+ } from "./chunk-2FPGJKCA.mjs";
15
15
  import {
16
16
  PerplexityClient,
17
17
  exportThreadViaImpit,
18
18
  readCachedAccountInfoFromDisk,
19
19
  retrieveThreadViaImpit
20
- } from "./chunk-H4BUAPPO.mjs";
20
+ } from "./chunk-C3HPFFTD.mjs";
21
21
  import {
22
22
  append,
23
23
  findPendingByThread,
@@ -26,13 +26,13 @@ import {
26
26
  getHistoryDir,
27
27
  list,
28
28
  update
29
- } from "./chunk-OF4DMAPJ.mjs";
29
+ } from "./chunk-B65IJQZJ.mjs";
30
30
  import {
31
31
  safeAtomicWriteFileSync
32
32
  } from "./chunk-MTDFKNXX.mjs";
33
33
  import {
34
34
  getConfigDir
35
- } from "./chunk-XKSWCEGI.mjs";
35
+ } from "./chunk-HJIXH6CL.mjs";
36
36
 
37
37
  // src/daemon/server.ts
38
38
  import { createServer } from "http";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-MTDFKNXX.mjs";
4
4
  import {
5
5
  getConfigDir
6
- } from "./chunk-XKSWCEGI.mjs";
6
+ } from "./chunk-HJIXH6CL.mjs";
7
7
 
8
8
  // src/daemon/token.ts
9
9
  import { randomBytes } from "crypto";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  Vault
3
- } from "./chunk-TQLCLE4L.mjs";
3
+ } from "./chunk-S677V2JU.mjs";
4
4
  import {
5
5
  getActiveName,
6
6
  getProfilePaths
7
- } from "./chunk-XKSWCEGI.mjs";
7
+ } from "./chunk-HJIXH6CL.mjs";
8
8
 
9
9
  // src/config.ts
10
10
  import { existsSync } 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/audit.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";