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.
- package/README.md +203 -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-SVPRB62V.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.d.ts +5 -1
- package/dist/index.mjs +96 -24
- 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
|
@@ -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";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfigDir,
|
|
3
|
+
getProfilePaths
|
|
4
|
+
} from "./chunk-HJIXH6CL.mjs";
|
|
5
|
+
|
|
6
|
+
// src/reinit-watcher.js
|
|
7
|
+
import { existsSync, mkdirSync, watch } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
function watchReinit(profileName, callback, opts = {}) {
|
|
10
|
+
const { debounceMs = 200 } = opts;
|
|
11
|
+
const target = getProfilePaths(profileName).reinit;
|
|
12
|
+
const parent = dirname(target);
|
|
13
|
+
if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
|
|
14
|
+
let timer = null;
|
|
15
|
+
const w = watch(parent, { persistent: false }, (event, filename) => {
|
|
16
|
+
if (!filename) return;
|
|
17
|
+
if (!String(filename).endsWith(".reinit")) return;
|
|
18
|
+
if (!existsSync(target)) return;
|
|
19
|
+
if (timer) clearTimeout(timer);
|
|
20
|
+
timer = setTimeout(() => {
|
|
21
|
+
timer = null;
|
|
22
|
+
try {
|
|
23
|
+
callback();
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}, debounceMs);
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
dispose() {
|
|
30
|
+
if (timer) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
timer = null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
w.close();
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function watchActiveProfile(configDirOverride, callback, opts = {}) {
|
|
42
|
+
const { debounceMs = 200 } = opts;
|
|
43
|
+
const configDir = configDirOverride ?? getConfigDir();
|
|
44
|
+
const target = join(configDir, "active");
|
|
45
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
|
46
|
+
let timer = null;
|
|
47
|
+
const w = watch(configDir, { persistent: false }, (event, filename) => {
|
|
48
|
+
if (!filename) return;
|
|
49
|
+
if (String(filename) !== "active") return;
|
|
50
|
+
if (!existsSync(target)) return;
|
|
51
|
+
if (timer) clearTimeout(timer);
|
|
52
|
+
timer = setTimeout(() => {
|
|
53
|
+
timer = null;
|
|
54
|
+
try {
|
|
55
|
+
callback();
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}, debounceMs);
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
dispose() {
|
|
62
|
+
if (timer) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
timer = null;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
w.close();
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
watchReinit,
|
|
76
|
+
watchActiveProfile
|
|
77
|
+
};
|
package/dist/cli.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ function parseArgs(argv) {
|
|
|
62
62
|
|
|
63
63
|
const KNOWN_COMMANDS = new Set([
|
|
64
64
|
"server", "version", "help",
|
|
65
|
-
"login", "logout", "status", "doctor", "install-browser",
|
|
65
|
+
"login", "logout", "status", "doctor", "install-browser", "setup-vault",
|
|
66
66
|
"install-speed-boost", "uninstall-speed-boost", "speed-boost-status",
|
|
67
67
|
"add-account", "switch-account", "list-accounts",
|
|
68
68
|
"export", "open", "rebuild-history-index", "sync-cloud",
|
|
@@ -80,6 +80,288 @@ function normalizeExportFormat(value) {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Probe the full vault-unseal state for the current process.
|
|
85
|
+
*
|
|
86
|
+
* Returns a structured snapshot covering every input the runtime path
|
|
87
|
+
* (vault.js getUnsealMaterial) considers: keychain availability and
|
|
88
|
+
* whether the master key was persisted there, env-var passphrase, TTY
|
|
89
|
+
* fallback, and (when a profile is given) whether the on-disk vault.enc
|
|
90
|
+
* actually decrypts with the resolved unseal material. The setup-vault
|
|
91
|
+
* command and the add-account/login preflight share this so user-facing
|
|
92
|
+
* advice stays consistent with what the runner will actually do.
|
|
93
|
+
*/
|
|
94
|
+
async function probeVaultState({ profile } = {}) {
|
|
95
|
+
let keychainAvailable = false;
|
|
96
|
+
let keychainHasKey = false;
|
|
97
|
+
try {
|
|
98
|
+
const mod = await import('keytar');
|
|
99
|
+
const keytar = mod.default ?? mod;
|
|
100
|
+
if (keytar && typeof keytar.getPassword === "function") {
|
|
101
|
+
keychainAvailable = true;
|
|
102
|
+
try {
|
|
103
|
+
const hex = await keytar.getPassword("perplexity-user-mcp", "vault-master-key");
|
|
104
|
+
keychainHasKey = !!hex;
|
|
105
|
+
} catch {
|
|
106
|
+
// getPassword can throw on broken credstore backends (e.g. headless
|
|
107
|
+
// Linux without libsecret). The binding loaded but isn't usable —
|
|
108
|
+
// treat that as "available but no key", same posture as a fresh
|
|
109
|
+
// box. vault.js falls back to env var when keychain returns null.
|
|
110
|
+
keychainHasKey = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
keychainAvailable = false;
|
|
115
|
+
}
|
|
116
|
+
const envPassphraseSet = !!process.env.PERPLEXITY_VAULT_PASSPHRASE;
|
|
117
|
+
const hasTty = process.stdin?.isTTY === true && process.env.PERPLEXITY_MCP_STDIO !== "1";
|
|
118
|
+
|
|
119
|
+
let vaultExists = false;
|
|
120
|
+
let vaultDecryptsOk = null;
|
|
121
|
+
let decryptError = null;
|
|
122
|
+
if (profile) {
|
|
123
|
+
try {
|
|
124
|
+
const { getProfilePaths } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
125
|
+
const { existsSync } = await import('node:fs');
|
|
126
|
+
vaultExists = existsSync(getProfilePaths(profile).vault);
|
|
127
|
+
if (vaultExists && (keychainAvailable || envPassphraseSet)) {
|
|
128
|
+
const { Vault, __resetKeyCache } = await import('./vault.d-BSJWDLhp.d.ts');
|
|
129
|
+
__resetKeyCache();
|
|
130
|
+
try {
|
|
131
|
+
await new Vault().get(profile, "cookies");
|
|
132
|
+
vaultDecryptsOk = true;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
vaultDecryptsOk = false;
|
|
135
|
+
decryptError = err instanceof Error ? err.message : String(err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// Probe is best-effort; don't crash the CLI just because the
|
|
140
|
+
// profile dir or modules failed to load.
|
|
141
|
+
decryptError = err instanceof Error ? err.message : String(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
platform: process.platform,
|
|
147
|
+
keychainAvailable,
|
|
148
|
+
keychainHasKey,
|
|
149
|
+
envPassphraseSet,
|
|
150
|
+
hasTty,
|
|
151
|
+
vaultExists,
|
|
152
|
+
vaultDecryptsOk,
|
|
153
|
+
decryptError,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Surface vault-unseal status BEFORE the user kicks off an interactive
|
|
159
|
+
* operation (add-account, login). Returns {ok: true} when at least one
|
|
160
|
+
* unseal path is configured. Returns {ok: false, ...} with structured
|
|
161
|
+
* guidance when none of those are available — caller decides whether to
|
|
162
|
+
* warn-then-continue or hard-stop.
|
|
163
|
+
*/
|
|
164
|
+
async function checkVaultUnseal() {
|
|
165
|
+
const state = await probeVaultState();
|
|
166
|
+
if (state.keychainAvailable || state.envPassphraseSet || state.hasTty) {
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
hasKeychain: state.keychainAvailable,
|
|
170
|
+
envPass: state.envPassphraseSet,
|
|
171
|
+
hasTty: state.hasTty,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const isLinux = state.platform === "linux";
|
|
175
|
+
const isMac = state.platform === "darwin";
|
|
176
|
+
const isWin = state.platform === "win32";
|
|
177
|
+
const hint = isLinux
|
|
178
|
+
? "Install libsecret + gnome-keyring (Debian/Ubuntu: `sudo apt install libsecret-1-0 gnome-keyring`; Fedora: `sudo dnf install libsecret gnome-keyring`), OR run `npx perplexity-user-mcp setup-vault` to generate a passphrase and get persistence snippets for your shell / MCP-client config."
|
|
179
|
+
: isMac
|
|
180
|
+
? "Keychain Access should be available on macOS — keytar usually loads here. If you still see this, run `npx perplexity-user-mcp setup-vault` for a generated passphrase + persistence snippets."
|
|
181
|
+
: isWin
|
|
182
|
+
? "Credential Manager is always available on Windows — keytar usually loads here. If you still see this, run `npx perplexity-user-mcp setup-vault` for a generated passphrase + persistence snippets."
|
|
183
|
+
: "Run `npx perplexity-user-mcp setup-vault` for a generated passphrase + persistence snippets.";
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
hasKeychain: false,
|
|
187
|
+
envPass: false,
|
|
188
|
+
hasTty: false,
|
|
189
|
+
reason: "no_unseal_material",
|
|
190
|
+
hint,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate a strong random passphrase encoded as a shell-safe base64url
|
|
196
|
+
* string (no `+`, `/`, or `=` so it can be pasted into shell rcs and JSON
|
|
197
|
+
* env blocks without escaping). 32 bytes = 256 bits of entropy, matching
|
|
198
|
+
* the AES key strength so the passphrase is never the weak link.
|
|
199
|
+
*/
|
|
200
|
+
async function generatePassphrase() {
|
|
201
|
+
const { randomBytes } = await import('node:crypto');
|
|
202
|
+
// base64url encoding in Node ≥16.
|
|
203
|
+
return randomBytes(32).toString("base64url");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build platform-specific persistence snippets the user can copy into
|
|
208
|
+
* their environment. Kept format-agnostic — does NOT write any file —
|
|
209
|
+
* because the safest place to put a passphrase varies by deployment
|
|
210
|
+
* (per-IDE mcp.json env block, ~/.zshrc, systemd unit, Docker secret).
|
|
211
|
+
*/
|
|
212
|
+
function buildPersistenceSnippets(passphrase) {
|
|
213
|
+
const platform = process.platform;
|
|
214
|
+
const snippets = [];
|
|
215
|
+
|
|
216
|
+
// 1. MCP client env block (preferred — scoped per client).
|
|
217
|
+
snippets.push({
|
|
218
|
+
title: "MCP client env block (preferred — scoped to one client only)",
|
|
219
|
+
detail: "Edit your MCP client's config (Cursor: ~/.cursor/mcp.json, Claude Desktop: claude_desktop_config.json, Codex CLI: ~/.codex/config.toml, etc.). Add an `env` field next to `command`/`args`:",
|
|
220
|
+
code: `{
|
|
221
|
+
"mcpServers": {
|
|
222
|
+
"Perplexity": {
|
|
223
|
+
"command": "npx",
|
|
224
|
+
"args": ["-y", "perplexity-user-mcp"],
|
|
225
|
+
"env": {
|
|
226
|
+
"PERPLEXITY_VAULT_PASSPHRASE": "${passphrase}"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}`,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// 2. Shell rc — platform-specific.
|
|
234
|
+
if (platform === "win32") {
|
|
235
|
+
snippets.push({
|
|
236
|
+
title: "Windows — PowerShell user environment (persistent)",
|
|
237
|
+
detail: "Sets the variable for your user account; persists across reboots. Open PowerShell and run:",
|
|
238
|
+
code: `[Environment]::SetEnvironmentVariable("PERPLEXITY_VAULT_PASSPHRASE", "${passphrase}", "User")`,
|
|
239
|
+
});
|
|
240
|
+
snippets.push({
|
|
241
|
+
title: "Windows — cmd.exe (persistent)",
|
|
242
|
+
detail: "Equivalent for cmd.exe users:",
|
|
243
|
+
code: `setx PERPLEXITY_VAULT_PASSPHRASE "${passphrase}"`,
|
|
244
|
+
});
|
|
245
|
+
} else if (platform === "darwin") {
|
|
246
|
+
snippets.push({
|
|
247
|
+
title: "macOS — zsh (default since Catalina)",
|
|
248
|
+
detail: "Append to ~/.zshrc and restart your terminal:",
|
|
249
|
+
code: `echo 'export PERPLEXITY_VAULT_PASSPHRASE='\\''${passphrase}'\\''' >> ~/.zshrc`,
|
|
250
|
+
});
|
|
251
|
+
snippets.push({
|
|
252
|
+
title: "macOS — bash (legacy)",
|
|
253
|
+
detail: "If you use bash instead, append to ~/.bash_profile:",
|
|
254
|
+
code: `echo 'export PERPLEXITY_VAULT_PASSPHRASE='\\''${passphrase}'\\''' >> ~/.bash_profile`,
|
|
255
|
+
});
|
|
256
|
+
} else {
|
|
257
|
+
// Linux + everything else
|
|
258
|
+
snippets.push({
|
|
259
|
+
title: "Linux — bash",
|
|
260
|
+
detail: "Append to ~/.bashrc and restart your terminal (or `source ~/.bashrc`):",
|
|
261
|
+
code: `echo 'export PERPLEXITY_VAULT_PASSPHRASE='\\''${passphrase}'\\''' >> ~/.bashrc`,
|
|
262
|
+
});
|
|
263
|
+
snippets.push({
|
|
264
|
+
title: "Linux — zsh",
|
|
265
|
+
detail: "If you use zsh, append to ~/.zshrc:",
|
|
266
|
+
code: `echo 'export PERPLEXITY_VAULT_PASSPHRASE='\\''${passphrase}'\\''' >> ~/.zshrc`,
|
|
267
|
+
});
|
|
268
|
+
snippets.push({
|
|
269
|
+
title: "Linux — systemd unit (for daemon deployments)",
|
|
270
|
+
detail: "If you run perplexity-user-mcp as a systemd service, add to the [Service] block:",
|
|
271
|
+
code: `Environment=PERPLEXITY_VAULT_PASSPHRASE=${passphrase}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return snippets;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Render a plain-text setup-vault report. Used for the default human-
|
|
280
|
+
* readable output. JSON output uses `--json` and bypasses this entirely.
|
|
281
|
+
*/
|
|
282
|
+
function renderSetupVaultReport({ state, recommendation, passphrase, snippets }) {
|
|
283
|
+
const lines = [];
|
|
284
|
+
const tick = "✓";
|
|
285
|
+
const cross = "✗";
|
|
286
|
+
const warn = "!";
|
|
287
|
+
lines.push("Vault setup status:");
|
|
288
|
+
lines.push(` ${state.keychainAvailable ? tick : cross} OS keychain ${state.keychainAvailable ? "available" : "unavailable"}${state.keychainHasKey ? " (master key persisted)" : state.keychainAvailable ? " (no master key yet — will be generated on first login)" : ""}`);
|
|
289
|
+
lines.push(` ${state.envPassphraseSet ? tick : cross} PERPLEXITY_VAULT_PASSPHRASE ${state.envPassphraseSet ? "is set" : "is not set"}`);
|
|
290
|
+
if (state.vaultExists) {
|
|
291
|
+
if (state.vaultDecryptsOk === true) {
|
|
292
|
+
lines.push(` ${tick} vault.enc decrypts cleanly with the active unseal material`);
|
|
293
|
+
} else if (state.vaultDecryptsOk === false) {
|
|
294
|
+
lines.push(` ${cross} vault.enc cannot be decrypted — ${state.decryptError ?? "unknown error"}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (state.keychainAvailable && state.envPassphraseSet) {
|
|
298
|
+
lines.push(` ${warn} both keychain and env var are set — keychain wins at runtime; the env var is a fallback`);
|
|
299
|
+
}
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push(`Recommendation: ${recommendation.message}`);
|
|
302
|
+
|
|
303
|
+
if (passphrase) {
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("Generated passphrase (256 bits, base64url):");
|
|
306
|
+
lines.push(` ${passphrase}`);
|
|
307
|
+
lines.push("");
|
|
308
|
+
lines.push("⚠ Save this somewhere safe — losing it means losing access to vaults written under it.");
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push("Pick ONE persistence method below:");
|
|
311
|
+
snippets.forEach((s, i) => {
|
|
312
|
+
lines.push("");
|
|
313
|
+
lines.push(`${i + 1}. ${s.title}`);
|
|
314
|
+
if (s.detail) lines.push(` ${s.detail}`);
|
|
315
|
+
lines.push("");
|
|
316
|
+
const indent = " ";
|
|
317
|
+
lines.push(s.code.split("\n").map((l) => indent + l).join("\n"));
|
|
318
|
+
});
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("After applying ONE of those, run `npx perplexity-user-mcp doctor` to verify the unseal-verify check passes.");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return lines.join("\n");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Decide what the user should do given the probed vault state.
|
|
328
|
+
*
|
|
329
|
+
* - keychain works + vault decrypts (or no vault yet) → nothing to do.
|
|
330
|
+
* - keychain works + vault fails to decrypt → tell user to logout --purge.
|
|
331
|
+
* - no keychain + env var set → done; vault will use passphrase.
|
|
332
|
+
* - no keychain + no env var → setup needed; generate + show snippets.
|
|
333
|
+
*/
|
|
334
|
+
function recommendVaultSetup(state) {
|
|
335
|
+
if (state.vaultExists && state.vaultDecryptsOk === false) {
|
|
336
|
+
return {
|
|
337
|
+
status: "decrypt_broken",
|
|
338
|
+
message: "Existing vault.enc cannot be decrypted with any available unseal material. The blob was likely written under a since-rotated keychain key or PERPLEXITY_VAULT_PASSPHRASE. Run `npx perplexity-user-mcp logout --purge --profile <name>` and log in again to write a fresh vault. (v0.8.40+ self-heals this on the next login by quarantining the bad blob.)",
|
|
339
|
+
generatePassphrase: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (state.keychainAvailable) {
|
|
343
|
+
return {
|
|
344
|
+
status: "ok_keychain",
|
|
345
|
+
message: state.keychainHasKey
|
|
346
|
+
? "OS keychain holds the master key — nothing to do."
|
|
347
|
+
: "OS keychain is available; the master key will be generated and persisted there on your first login. Nothing to do.",
|
|
348
|
+
generatePassphrase: false,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (state.envPassphraseSet) {
|
|
352
|
+
return {
|
|
353
|
+
status: "ok_envvar",
|
|
354
|
+
message: "PERPLEXITY_VAULT_PASSPHRASE is set; the vault will use it. (For better UX, install an OS keychain so the env var becomes optional — see https://github.com/Automations-Project/VSCode-Perplexity-MCP for platform docs.)",
|
|
355
|
+
generatePassphrase: false,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
status: "setup_needed",
|
|
360
|
+
message: "No keychain available and no PERPLEXITY_VAULT_PASSPHRASE set. Generating a strong passphrase and showing persistence snippets below.",
|
|
361
|
+
generatePassphrase: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
83
365
|
async function openTarget(target) {
|
|
84
366
|
if (process.platform === "win32") {
|
|
85
367
|
const escaped = String(target).replace(/'/g, "''");
|
|
@@ -478,9 +760,50 @@ async function routeCommand(parsed) {
|
|
|
478
760
|
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
479
761
|
}
|
|
480
762
|
|
|
763
|
+
if (command === "setup-vault") {
|
|
764
|
+
const profile = flags.profile ?? (await import('./profiles.d-DqS1oZWr.d.ts')).getActiveName() ?? null;
|
|
765
|
+
const state = await probeVaultState({ profile });
|
|
766
|
+
const recommendation = recommendVaultSetup(state);
|
|
767
|
+
let passphrase = null;
|
|
768
|
+
let snippets = [];
|
|
769
|
+
if (recommendation.generatePassphrase && !flags["probe-only"]) {
|
|
770
|
+
passphrase = await generatePassphrase();
|
|
771
|
+
snippets = buildPersistenceSnippets(passphrase);
|
|
772
|
+
}
|
|
773
|
+
if (flags.json) {
|
|
774
|
+
const body = JSON.stringify({
|
|
775
|
+
ok: true,
|
|
776
|
+
state,
|
|
777
|
+
recommendation: { status: recommendation.status, message: recommendation.message },
|
|
778
|
+
passphrase: passphrase ?? null,
|
|
779
|
+
snippets: snippets.map((s) => ({ title: s.title, detail: s.detail, code: s.code })),
|
|
780
|
+
});
|
|
781
|
+
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
782
|
+
}
|
|
783
|
+
const report = renderSetupVaultReport({ state, recommendation, passphrase, snippets });
|
|
784
|
+
return { code: 0, stdout: report + "\n", stderr: "" };
|
|
785
|
+
}
|
|
786
|
+
|
|
481
787
|
if (command === "add-account") {
|
|
482
788
|
const name = flags.name ?? (await import('./profiles.d-DqS1oZWr.d.ts')).suggestNextDefaultName();
|
|
483
789
|
const mode = flags.mode ?? "manual";
|
|
790
|
+
|
|
791
|
+
// Pre-flight the unseal chain BEFORE touching the profile dir, so users
|
|
792
|
+
// creating a new account on a fresh box get an actionable setup hint
|
|
793
|
+
// instead of a "Vault decrypt failed" / "Vault locked" surprise on the
|
|
794
|
+
// first login. Bypass with --skip-vault-check (e.g. when the daemon
|
|
795
|
+
// owns the vault and the CLI is just used for account management).
|
|
796
|
+
if (!flags["skip-vault-check"]) {
|
|
797
|
+
const unseal = await checkVaultUnseal();
|
|
798
|
+
if (!unseal.ok) {
|
|
799
|
+
const msg = `No vault unseal path configured. ${unseal.hint}`;
|
|
800
|
+
const body = flags.json
|
|
801
|
+
? JSON.stringify({ ok: false, reason: unseal.reason, hint: unseal.hint })
|
|
802
|
+
: "";
|
|
803
|
+
return { code: 1, stdout: body + (body ? "\n" : ""), stderr: msg + "\n" };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
484
807
|
try {
|
|
485
808
|
const { createProfile } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
486
809
|
const profile = createProfile(name, { loginMode: mode });
|
|
@@ -515,7 +838,7 @@ async function routeCommand(parsed) {
|
|
|
515
838
|
|
|
516
839
|
if (command === "status") {
|
|
517
840
|
const name = flags.profile ?? (await import('./profiles.d-DqS1oZWr.d.ts')).getActiveName() ?? "default";
|
|
518
|
-
const { Vault } = await import('./vault.d-
|
|
841
|
+
const { Vault } = await import('./vault.d-BSJWDLhp.d.ts');
|
|
519
842
|
/* v8 ignore next -- defensive catch for unreadable vault (malformed blob, wrong key) */
|
|
520
843
|
const cookies = await new Vault().get(name, "cookies").catch(() => null);
|
|
521
844
|
if (!cookies) {
|
|
@@ -535,6 +858,22 @@ async function routeCommand(parsed) {
|
|
|
535
858
|
const { fork } = await import('node:child_process');
|
|
536
859
|
const mode = flags.mode ?? "manual";
|
|
537
860
|
const profile = flags.profile ?? (await import('./profiles.d-DqS1oZWr.d.ts')).getActiveName() ?? "default";
|
|
861
|
+
|
|
862
|
+
// Same preflight as add-account: surface unseal-path setup BEFORE the
|
|
863
|
+
// browser opens, so a user on a fresh headless box doesn't complete a
|
|
864
|
+
// 30s login flow only to crash at vault.set with a stack trace. Skip
|
|
865
|
+
// when --plain-cookies is set since plaintext mode bypasses the vault.
|
|
866
|
+
if (!flags["plain-cookies"] && !flags["skip-vault-check"]) {
|
|
867
|
+
const unseal = await checkVaultUnseal();
|
|
868
|
+
if (!unseal.ok) {
|
|
869
|
+
const msg = `No vault unseal path configured for profile '${profile}'. ${unseal.hint}`;
|
|
870
|
+
const body = flags.json
|
|
871
|
+
? JSON.stringify({ ok: false, reason: unseal.reason, hint: unseal.hint, profile })
|
|
872
|
+
: "";
|
|
873
|
+
return { code: 1, stdout: body + (body ? "\n" : ""), stderr: msg + "\n" };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
538
877
|
const env = { ...process.env, PERPLEXITY_PROFILE: profile };
|
|
539
878
|
if (mode === "auto") {
|
|
540
879
|
if (!flags.email) return { code: 1, stdout: "", stderr: "`--email` required for --mode auto.\n" };
|
|
@@ -881,6 +1220,13 @@ Usage:
|
|
|
881
1220
|
npx perplexity-user-mcp status [--profile X] [--all]
|
|
882
1221
|
npx perplexity-user-mcp doctor [--profile X] [--probe] [--all] [--report]
|
|
883
1222
|
npx perplexity-user-mcp install-browser
|
|
1223
|
+
npx perplexity-user-mcp setup-vault [--profile X] [--json] [--probe-only]
|
|
1224
|
+
Probe the vault unseal chain (OS keychain / PERPLEXITY_VAULT_PASSPHRASE)
|
|
1225
|
+
and, when neither is configured, generate a strong passphrase and print
|
|
1226
|
+
cross-platform persistence snippets (PowerShell / setx / zsh / bash /
|
|
1227
|
+
systemd / MCP-client env block). Read-only — never writes any file.
|
|
1228
|
+
Cross-platform: Windows, macOS, Linux. Add --probe-only to skip
|
|
1229
|
+
passphrase generation and just report state.
|
|
884
1230
|
npx perplexity-user-mcp install-speed-boost [--force] [--json]
|
|
885
1231
|
npx perplexity-user-mcp uninstall-speed-boost [--json]
|
|
886
1232
|
npx perplexity-user-mcp speed-boost-status [--json]
|