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.
- package/README.md +117 -9
- package/dist/checks/vault.d.ts +41 -0
- package/dist/checks/vault.mjs +23 -0
- package/dist/{chunk-Q2VY4R5F.mjs → chunk-2FPGJKCA.mjs} +2 -2
- package/dist/{chunk-ZQFUZPLO.mjs → chunk-452DK6OS.mjs} +2 -2
- package/dist/{chunk-OF4DMAPJ.mjs → chunk-B65IJQZJ.mjs} +1 -1
- package/dist/{chunk-H4BUAPPO.mjs → chunk-C3HPFFTD.mjs} +4 -4
- package/dist/{chunk-LZPLNZ5U.mjs → chunk-D254EFYB.mjs} +1 -1
- package/dist/{chunk-Z7DAACGZ.mjs → chunk-DQQISMYN.mjs} +2 -2
- package/dist/{chunk-3B276PGG.mjs → chunk-FKQ3HP4Q.mjs} +1 -1
- package/dist/{chunk-7JL36EBH.mjs → chunk-HNSPNCFH.mjs} +1 -1
- package/dist/{chunk-6EP2BLTV.mjs → chunk-KJFX2ZXR.mjs} +1 -1
- package/dist/{chunk-YUGDOXIN.mjs → chunk-NJX4RBO6.mjs} +1 -1
- package/dist/{chunk-X45O6YD3.mjs → chunk-RK4EBZJ3.mjs} +28 -9
- package/dist/{chunk-TQLCLE4L.mjs → chunk-S677V2JU.mjs} +57 -12
- package/dist/{chunk-S5VD7WTU.mjs → chunk-T6ARJK2P.mjs} +6 -6
- package/dist/{chunk-HTUAQRKH.mjs → chunk-TDXETAQT.mjs} +1 -1
- package/dist/{chunk-LKJMLGFP.mjs → chunk-U7QPUNRH.mjs} +2 -2
- package/dist/{chunk-PE23RMXY.mjs → chunk-V4U3JM4R.mjs} +1 -1
- package/dist/chunk-WDIW33DA.mjs +77 -0
- package/dist/{chunk-KCXM2M4B.mjs → chunk-XTRJSV72.mjs} +1 -1
- package/dist/cli.d.ts +348 -2
- package/dist/cli.mjs +259 -3
- package/dist/client.mjs +6 -6
- package/dist/cloud-sync.mjs +8 -8
- package/dist/config.mjs +3 -3
- package/dist/daemon/attach.mjs +17 -17
- package/dist/daemon/audit.mjs +2 -2
- package/dist/daemon/client-http.mjs +17 -17
- package/dist/daemon/index.mjs +18 -18
- package/dist/daemon/install-tunnel.mjs +2 -2
- package/dist/daemon/launcher.mjs +16 -16
- package/dist/daemon/lockfile.mjs +2 -2
- package/dist/daemon/server.mjs +11 -11
- package/dist/daemon/token.mjs +2 -2
- package/dist/daemon/tunnel-providers/index.mjs +3 -3
- package/dist/doctor.mjs +2 -2
- package/dist/export.mjs +4 -4
- package/dist/health-check.d.ts +1 -1
- package/dist/health-check.mjs +3 -3
- package/dist/history-store.mjs +2 -2
- package/dist/impit-login-runner.d.ts +1 -1
- package/dist/impit-login-runner.mjs +4 -4
- package/dist/index.mjs +57 -22
- package/dist/login-runner.d.ts +1 -1
- package/dist/login-runner.mjs +3 -3
- package/dist/logout.d.ts +1 -1
- package/dist/logout.mjs +2 -2
- package/dist/manual-login-runner.d.ts +1 -1
- package/dist/manual-login-runner.mjs +3 -3
- package/dist/{native-deps-YNKXITRY.mjs → native-deps-IE4B55EL.mjs} +4 -4
- package/dist/profiles.mjs +1 -1
- package/dist/refresh.mjs +4 -4
- package/dist/reinit-watcher.d.ts +12 -1
- package/dist/reinit-watcher.mjs +4 -2
- package/dist/vault.d-BSJWDLhp.d.ts +37 -0
- package/dist/vault.mjs +4 -2
- package/dist/viewers.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-U3DGFLXZ.mjs +0 -43
- package/dist/vault.d-BtRSLZiM.d.ts +0 -8
- /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
|
-
|
|
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
|
-
|
|
29
|
-
npx perplexity-user-mcp
|
|
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
|
-
|
|
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
|
|
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 |
|
package/dist/checks/vault.d.ts
CHANGED
|
@@ -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
|
|
package/dist/checks/vault.mjs
CHANGED
|
@@ -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-
|
|
5
|
+
} from "./chunk-C3HPFFTD.mjs";
|
|
6
6
|
import {
|
|
7
7
|
hydrateCloudEntry,
|
|
8
8
|
upsertFromCloud
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-B65IJQZJ.mjs";
|
|
10
10
|
|
|
11
11
|
// src/cloud-sync.js
|
|
12
12
|
var DEFAULT_PAGE_SIZE = 1e3;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
impitFetchJson,
|
|
3
3
|
isImpitAvailable
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DQQISMYN.mjs";
|
|
5
5
|
import {
|
|
6
6
|
FORMAT_TO_CONTENT_TYPE,
|
|
7
7
|
exportThread,
|
|
8
8
|
resolveExportApiFormat
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
24
|
+
} from "./chunk-U7QPUNRH.mjs";
|
|
25
25
|
import {
|
|
26
26
|
getActiveName,
|
|
27
27
|
getConfigDir,
|
|
28
28
|
getProfilePaths
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-HJIXH6CL.mjs";
|
|
30
30
|
|
|
31
31
|
// src/client.ts
|
|
32
32
|
import { randomUUID } from "crypto";
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
getOrCreateContext,
|
|
10
10
|
getSavedCookies,
|
|
11
11
|
resolveBrowserExecutable
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-U7QPUNRH.mjs";
|
|
13
13
|
import {
|
|
14
14
|
getActiveName,
|
|
15
15
|
getConfigDir,
|
|
16
16
|
getProfilePaths
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-HJIXH6CL.mjs";
|
|
18
18
|
|
|
19
19
|
// src/refresh.ts
|
|
20
20
|
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from "fs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getTunnelProvider,
|
|
3
3
|
readTunnelSettings
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
12
|
+
} from "./chunk-KJFX2ZXR.mjs";
|
|
13
13
|
import {
|
|
14
14
|
getPackageVersion,
|
|
15
15
|
startDaemonServer
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-T6ARJK2P.mjs";
|
|
17
17
|
import {
|
|
18
18
|
ensureToken,
|
|
19
19
|
getTokenPath,
|
|
20
20
|
readToken
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-TDXETAQT.mjs";
|
|
22
22
|
import {
|
|
23
|
+
watchActiveProfile,
|
|
23
24
|
watchReinit
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-WDIW33DA.mjs";
|
|
25
26
|
import {
|
|
26
27
|
PerplexityClient
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-C3HPFFTD.mjs";
|
|
28
29
|
import {
|
|
29
30
|
getActiveName,
|
|
30
31
|
getConfigDir
|
|
31
|
-
} from "./chunk-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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-
|
|
5
|
+
} from "./chunk-V4U3JM4R.mjs";
|
|
6
6
|
import {
|
|
7
7
|
ensureToken,
|
|
8
8
|
getTokenPath,
|
|
9
9
|
rotateToken
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-TDXETAQT.mjs";
|
|
11
11
|
import {
|
|
12
12
|
hydrateCloudHistoryEntry,
|
|
13
13
|
syncCloudHistory
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2FPGJKCA.mjs";
|
|
15
15
|
import {
|
|
16
16
|
PerplexityClient,
|
|
17
17
|
exportThreadViaImpit,
|
|
18
18
|
readCachedAccountInfoFromDisk,
|
|
19
19
|
retrieveThreadViaImpit
|
|
20
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
35
|
+
} from "./chunk-HJIXH6CL.mjs";
|
|
36
36
|
|
|
37
37
|
// src/daemon/server.ts
|
|
38
38
|
import { createServer } from "http";
|