perplexity-user-mcp 0.8.39 → 0.8.44
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 +5 -0
- package/dist/attachments.d.ts +10 -20
- package/dist/browser-window.d.ts +1 -0
- package/dist/cf-warmup.d.ts +32 -0
- package/dist/checks/browser.d.ts +12 -100
- package/dist/checks/config.d.ts +25 -91
- package/dist/checks/ide.d.ts +31 -89
- package/dist/checks/mcp.d.ts +12 -61
- package/dist/checks/native-deps.d.ts +46 -131
- package/dist/checks/network.d.ts +13 -71
- package/dist/checks/probe.d.ts +20 -92
- package/dist/checks/profiles.d.ts +13 -99
- package/dist/checks/profiles.mjs +35 -0
- package/dist/checks/runtime.d.ts +24 -89
- package/dist/checks/vault.d.ts +13 -142
- package/dist/checks/vault.mjs +6 -11
- package/dist/{chunk-T6ARJK2P.mjs → chunk-2B5OQUXR.mjs} +6 -6
- package/dist/{chunk-2FPGJKCA.mjs → chunk-2EE7MNP2.mjs} +2 -2
- package/dist/{chunk-NJX4RBO6.mjs → chunk-2OVLCZHU.mjs} +28 -3
- package/dist/{chunk-WDIW33DA.mjs → chunk-3LUO5ATM.mjs} +1 -1
- package/dist/{chunk-B65IJQZJ.mjs → chunk-6E6XTHTG.mjs} +1 -1
- package/dist/{chunk-S677V2JU.mjs → chunk-C5I7KXHK.mjs} +32 -2
- package/dist/{chunk-TDXETAQT.mjs → chunk-DKEJZ4FI.mjs} +1 -1
- package/dist/{chunk-U7QPUNRH.mjs → chunk-DXR6EEZH.mjs} +26 -7
- package/dist/{chunk-HJIXH6CL.mjs → chunk-E3GRJXXJ.mjs} +2 -0
- package/dist/{chunk-RK4EBZJ3.mjs → chunk-E75J42W5.mjs} +11 -8
- package/dist/{chunk-452DK6OS.mjs → chunk-FNHYUE22.mjs} +2 -2
- package/dist/{chunk-D254EFYB.mjs → chunk-GBI2U336.mjs} +1 -1
- package/dist/chunk-GPUGKWXH.mjs +17 -0
- package/dist/{chunk-HNSPNCFH.mjs → chunk-KSNV3ZVY.mjs} +1 -1
- package/dist/{chunk-XTRJSV72.mjs → chunk-LGH5BSUY.mjs} +1 -1
- package/dist/{chunk-KJFX2ZXR.mjs → chunk-NMKNEEZB.mjs} +1 -1
- package/dist/{chunk-FKQ3HP4Q.mjs → chunk-TIWHN4IW.mjs} +1 -1
- package/dist/{chunk-V4U3JM4R.mjs → chunk-TSLRTZYR.mjs} +1 -1
- package/dist/{chunk-DQQISMYN.mjs → chunk-V4LHDNWJ.mjs} +2 -2
- package/dist/{chunk-C3HPFFTD.mjs → chunk-WHVJ724K.mjs} +84 -44
- package/dist/cli.d.ts +14 -1298
- package/dist/cli.mjs +35 -27
- package/dist/client.d.ts +27 -24
- package/dist/client.mjs +6 -6
- package/dist/cloud-sync.d.ts +65 -42
- package/dist/cloud-sync.mjs +8 -8
- package/dist/config.d.ts +35 -39
- package/dist/config.mjs +3 -3
- package/dist/cookie-jar.d.ts +77 -0
- package/dist/daemon/attach.d.ts +10 -11
- package/dist/daemon/attach.mjs +19 -17
- package/dist/daemon/audit.d.ts +5 -7
- package/dist/daemon/audit.mjs +2 -2
- package/dist/daemon/client-http.d.ts +10 -16
- package/dist/daemon/client-http.mjs +17 -17
- package/dist/daemon/index.d.ts +17 -14
- package/dist/daemon/index.mjs +18 -18
- package/dist/daemon/install-tunnel.d.ts +8 -34
- package/dist/daemon/install-tunnel.mjs +2 -2
- package/dist/daemon/launcher.d.ts +24 -29
- package/dist/daemon/launcher.mjs +16 -16
- package/dist/daemon/local-tokens.d.ts +23 -0
- package/dist/daemon/lockfile.d.ts +10 -12
- package/dist/daemon/lockfile.mjs +2 -2
- package/dist/daemon/oauth-consent-cache.d.ts +86 -0
- package/dist/daemon/oauth-provider.d.ts +132 -0
- package/dist/daemon/public-pages.d.ts +9 -0
- package/dist/daemon/security.d.ts +52 -0
- package/dist/daemon/server.d.ts +12 -83
- package/dist/daemon/server.mjs +11 -11
- package/dist/daemon/token.d.ts +7 -9
- package/dist/daemon/token.mjs +2 -2
- package/dist/daemon/tunnel-providers/cloudflared-named-setup.d.ts +140 -0
- package/dist/daemon/tunnel-providers/cloudflared-named.d.ts +45 -0
- package/dist/daemon/tunnel-providers/cloudflared-quick.d.ts +8 -0
- package/dist/daemon/tunnel-providers/index.d.ts +16 -327
- package/dist/daemon/tunnel-providers/index.mjs +3 -3
- package/dist/daemon/tunnel-providers/ngrok-config.d.ts +18 -0
- package/dist/daemon/tunnel-providers/ngrok.d.ts +68 -0
- package/dist/daemon/tunnel-providers/types.d.ts +56 -0
- package/dist/daemon/tunnel.d.ts +5 -7
- package/dist/debug-tracer.d.ts +2 -0
- package/dist/doctor-report.d.ts +17 -22
- package/dist/doctor.d.ts +12 -44
- package/dist/doctor.mjs +2 -2
- package/dist/export.d.ts +11 -18
- package/dist/export.mjs +4 -4
- package/dist/format.d.ts +52 -0
- package/dist/fs-utils.d.ts +8 -0
- package/dist/health-check.d.ts +1 -108
- package/dist/health-check.mjs +3 -3
- package/dist/history-store.d.ts +29 -65
- package/dist/history-store.mjs +2 -2
- package/dist/impit-login-runner.d.ts +1 -469
- package/dist/impit-login-runner.mjs +4 -4
- package/dist/index.d.ts +25 -149
- package/dist/index.mjs +22 -20
- package/dist/is-main-module.d.ts +9 -0
- package/dist/login-runner.d.ts +1 -333
- package/dist/login-runner.mjs +13 -13
- package/dist/login.d.ts +5 -0
- package/dist/logout.d.ts +2 -28
- package/dist/logout.mjs +3 -2
- package/dist/manual-login-runner.d.ts +1 -150
- package/dist/manual-login-runner.mjs +11 -11
- package/dist/{native-deps-IE4B55EL.mjs → native-deps-FCSYDL4W.mjs} +4 -4
- package/dist/native-deps.d.ts +36 -0
- package/dist/package-version.d.ts +1 -0
- package/dist/profiles.d.ts +41 -41
- package/dist/profiles.mjs +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/redact.d.ts +14 -142
- package/dist/refresh.d.ts +11 -16
- package/dist/refresh.mjs +4 -4
- package/dist/reinit-watcher.d.ts +15 -24
- package/dist/reinit-watcher.mjs +2 -2
- package/dist/resources.d.ts +5 -0
- package/dist/safe-write.d.ts +16 -0
- package/dist/session-metadata.d.ts +45 -0
- package/dist/tool-config.d.ts +10 -0
- package/dist/tools.d.ts +23 -0
- package/dist/tty-prompt.d.ts +18 -34
- package/dist/vault.d.ts +114 -34
- package/dist/vault.mjs +6 -4
- package/dist/viewer-detect.d.ts +2 -4
- package/dist/viewers.d.ts +13 -18
- package/dist/viewers.mjs +1 -1
- package/package.json +3 -3
- package/dist/cloud-sync.d-Cqt6y18U.d.ts +0 -42
- package/dist/doctor.d-CXmUqOXX.d.ts +0 -43
- package/dist/history-store.d-BzjBF2m3.d.ts +0 -65
- package/dist/native-deps-BNThFHxa.d.ts +0 -175
- package/dist/profiles.d-DqS1oZWr.d.ts +0 -41
- package/dist/session-metadata-B9aV_n5g.d.ts +0 -148
- package/dist/vault.d-BSJWDLhp.d.ts +0 -37
- package/dist/viewer-detect.d-HWGnyFAA.d.ts +0 -4
- package/dist/viewers.d-BGCK6sw6.d.ts +0 -10
package/dist/cli.d.ts
CHANGED
|
@@ -1,1298 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function parseArgs(argv) {
|
|
16
|
-
if (argv.length === 0) return { command: "server", flags: {} };
|
|
17
|
-
const first = argv[0];
|
|
18
|
-
if (first === "--version" || first === "-v") return { command: "version", flags: {} };
|
|
19
|
-
if (first === "--help" || first === "-h") return { command: "help", flags: {} };
|
|
20
|
-
if (first === "daemon") {
|
|
21
|
-
const subcommand = argv[1] ?? "help";
|
|
22
|
-
const flags = {};
|
|
23
|
-
const positional = [];
|
|
24
|
-
for (let i = 2; i < argv.length; i++) {
|
|
25
|
-
const a = argv[i];
|
|
26
|
-
if (a.startsWith("--")) {
|
|
27
|
-
const key = a.slice(2);
|
|
28
|
-
const next = argv[i + 1];
|
|
29
|
-
if (next === undefined || next.startsWith("--")) {
|
|
30
|
-
flags[key] = true;
|
|
31
|
-
} else {
|
|
32
|
-
flags[key] = next;
|
|
33
|
-
i++;
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
positional.push(a);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return { command: `daemon:${subcommand}`, flags, positional };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const command = first;
|
|
43
|
-
const flags = {};
|
|
44
|
-
let positional = [];
|
|
45
|
-
for (let i = 1; i < argv.length; i++) {
|
|
46
|
-
const a = argv[i];
|
|
47
|
-
if (a.startsWith("--")) {
|
|
48
|
-
const key = a.slice(2);
|
|
49
|
-
const next = argv[i + 1];
|
|
50
|
-
if (next === undefined || next.startsWith("--")) {
|
|
51
|
-
flags[key] = true;
|
|
52
|
-
} else {
|
|
53
|
-
flags[key] = next;
|
|
54
|
-
i++;
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
positional.push(a);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return { command, flags, positional };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const KNOWN_COMMANDS = new Set([
|
|
64
|
-
"server", "version", "help",
|
|
65
|
-
"login", "logout", "status", "doctor", "install-browser", "setup-vault",
|
|
66
|
-
"install-speed-boost", "uninstall-speed-boost", "speed-boost-status",
|
|
67
|
-
"add-account", "switch-account", "list-accounts",
|
|
68
|
-
"export", "open", "rebuild-history-index", "sync-cloud",
|
|
69
|
-
"daemon:help", "daemon:start", "daemon:stop", "daemon:status", "daemon:attach",
|
|
70
|
-
"daemon:rotate-token", "daemon:install-tunnel", "daemon:enable-tunnel", "daemon:disable-tunnel",
|
|
71
|
-
"daemon:list-providers", "daemon:set-provider",
|
|
72
|
-
"daemon:set-ngrok-authtoken", "daemon:set-ngrok-domain", "daemon:clear-ngrok",
|
|
73
|
-
"daemon:cf-named-login", "daemon:cf-named-list",
|
|
74
|
-
"daemon:cf-named-create", "daemon:cf-named-bind",
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
function normalizeExportFormat(value) {
|
|
78
|
-
if (value === "md") return "markdown";
|
|
79
|
-
if (value === "markdown" || value === "pdf" || value === "docx") return value;
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
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
|
-
|
|
365
|
-
async function openTarget(target) {
|
|
366
|
-
if (process.platform === "win32") {
|
|
367
|
-
const escaped = String(target).replace(/'/g, "''");
|
|
368
|
-
await execFile("powershell", ["-NoProfile", "-Command", `Start-Process -FilePath '${escaped}'`]);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (process.platform === "darwin") {
|
|
372
|
-
await execFile("open", [String(target)]);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
await execFile("xdg-open", [String(target)]);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async function routeCommand(parsed) {
|
|
379
|
-
const { command, flags } = parsed;
|
|
380
|
-
if (!KNOWN_COMMANDS.has(command)) {
|
|
381
|
-
return { code: 1, stdout: "", stderr: `Unknown command: ${command}\nRun --help for usage.` };
|
|
382
|
-
}
|
|
383
|
-
if (command === "version") {
|
|
384
|
-
/* v8 ignore start -- catch fallback fires only if package.json is missing at runtime */
|
|
385
|
-
let version = "0.0.0";
|
|
386
|
-
try {
|
|
387
|
-
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
388
|
-
version = JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
389
|
-
} catch {
|
|
390
|
-
// fall through with default
|
|
391
|
-
}
|
|
392
|
-
/* v8 ignore stop */
|
|
393
|
-
return { code: 0, stdout: version + "\n", stderr: "" };
|
|
394
|
-
}
|
|
395
|
-
if (command === "help") {
|
|
396
|
-
return { code: 0, stdout: HELP_TEXT, stderr: "" };
|
|
397
|
-
}
|
|
398
|
-
/* v8 ignore start -- starting the real MCP server is impractical in unit tests */
|
|
399
|
-
if (command === "server") {
|
|
400
|
-
const { main } = await import('./index.d.ts');
|
|
401
|
-
await main();
|
|
402
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
403
|
-
}
|
|
404
|
-
/* v8 ignore stop */
|
|
405
|
-
|
|
406
|
-
if (command === "daemon:help") {
|
|
407
|
-
return { code: 0, stdout: DAEMON_HELP_TEXT, stderr: "" };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (command === "daemon:start") {
|
|
411
|
-
const port = parseOptionalPort(flags.port);
|
|
412
|
-
if (flags.port !== undefined && port === null) {
|
|
413
|
-
return { code: 1, stdout: "", stderr: "daemon start requires --port to be a positive integer.\n" };
|
|
414
|
-
}
|
|
415
|
-
const { startDaemon } = await import('./daemon/launcher.d.ts');
|
|
416
|
-
const daemon = await startDaemon({
|
|
417
|
-
configDir: process.env.PERPLEXITY_CONFIG_DIR,
|
|
418
|
-
port: port ?? undefined,
|
|
419
|
-
tunnel: !!flags.tunnel,
|
|
420
|
-
});
|
|
421
|
-
if (daemon.attached) {
|
|
422
|
-
const body = flags.json
|
|
423
|
-
? JSON.stringify({ ok: true, attached: true, ...serializeDaemonConnection(daemon) })
|
|
424
|
-
: `Attached to daemon pid=${daemon.pid} port=${daemon.port}`;
|
|
425
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
await daemon.closed;
|
|
429
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (command === "daemon:status") {
|
|
433
|
-
const { getDaemonStatus } = await import('./daemon/launcher.d.ts');
|
|
434
|
-
const status = await getDaemonStatus({
|
|
435
|
-
configDir: process.env.PERPLEXITY_CONFIG_DIR,
|
|
436
|
-
reclaimStale: true,
|
|
437
|
-
});
|
|
438
|
-
const body = flags.json
|
|
439
|
-
? JSON.stringify(serializeDaemonStatus(status))
|
|
440
|
-
: formatDaemonStatus(status);
|
|
441
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (command === "daemon:stop") {
|
|
445
|
-
const { stopDaemon } = await import('./daemon/launcher.d.ts');
|
|
446
|
-
const result = await stopDaemon({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
447
|
-
const body = flags.json
|
|
448
|
-
? JSON.stringify({ ok: true, ...result })
|
|
449
|
-
: result.stopped
|
|
450
|
-
? `Stopped daemon pid=${result.pid ?? "unknown"}.`
|
|
451
|
-
: "Daemon is not running.";
|
|
452
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (command === "daemon:rotate-token") {
|
|
456
|
-
try {
|
|
457
|
-
const { rotateDaemonToken } = await import('./daemon/launcher.d.ts');
|
|
458
|
-
const daemon = await rotateDaemonToken({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
459
|
-
const body = flags.json
|
|
460
|
-
? JSON.stringify({ ok: true, ...serializeDaemonConnection(daemon) })
|
|
461
|
-
: `Rotated daemon token for pid=${daemon.pid} port=${daemon.port}.`;
|
|
462
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
463
|
-
} catch (error) {
|
|
464
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
465
|
-
return { code: 1, stdout: "", stderr: message + "\n" };
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (command === "daemon:attach") {
|
|
470
|
-
// 8.3.2: PERPLEXITY_NO_DAEMON=1 opt-out. Must short-circuit BEFORE importing
|
|
471
|
-
// the daemon layer — the whole point is air-gapped / single-client users
|
|
472
|
-
// keep the daemon code cold. Warning goes to stderr only (stdout is the
|
|
473
|
-
// MCP JSON-RPC channel; any byte on stdout corrupts the protocol).
|
|
474
|
-
const noDaemonRaw = process.env.PERPLEXITY_NO_DAEMON;
|
|
475
|
-
if (typeof noDaemonRaw === "string" && /^(1|true)$/i.test(noDaemonRaw.trim())) {
|
|
476
|
-
process.stderr.write(
|
|
477
|
-
"[perplexity-mcp] PERPLEXITY_NO_DAEMON=1 set; running in-process stdio (daemon bypass)\n",
|
|
478
|
-
);
|
|
479
|
-
const mod = await import('./index.d.ts');
|
|
480
|
-
await mod.main();
|
|
481
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
482
|
-
}
|
|
483
|
-
const { attachToDaemon } = await import('./daemon/attach.d.ts');
|
|
484
|
-
const ensureTimeoutRaw = flags["ensure-timeout-ms"];
|
|
485
|
-
const ensureTimeoutMs =
|
|
486
|
-
typeof ensureTimeoutRaw === "string" && /^\d+$/.test(ensureTimeoutRaw)
|
|
487
|
-
? Number(ensureTimeoutRaw)
|
|
488
|
-
: undefined;
|
|
489
|
-
await attachToDaemon({
|
|
490
|
-
configDir: process.env.PERPLEXITY_CONFIG_DIR,
|
|
491
|
-
clientId: "daemon-attach-cli",
|
|
492
|
-
fallbackStdio: !!flags["fallback-stdio"],
|
|
493
|
-
ensureTimeoutMs,
|
|
494
|
-
});
|
|
495
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (command === "daemon:install-tunnel") {
|
|
499
|
-
const { installCloudflared } = await import('./daemon/install-tunnel.d.ts');
|
|
500
|
-
const result = await installCloudflared({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
501
|
-
const body = flags.json
|
|
502
|
-
? JSON.stringify({ ok: true, ...result })
|
|
503
|
-
: `Installed cloudflared ${result.version} to ${result.binaryPath}`;
|
|
504
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (command === "daemon:enable-tunnel") {
|
|
508
|
-
try {
|
|
509
|
-
const { enableDaemonTunnel } = await import('./daemon/launcher.d.ts');
|
|
510
|
-
const status = await enableDaemonTunnel({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
511
|
-
const body = flags.json
|
|
512
|
-
? JSON.stringify({ ok: true, ...serializeDaemonStatus(status) })
|
|
513
|
-
: status.health?.tunnel?.url
|
|
514
|
-
? `Tunnel enabled: ${status.health.tunnel.url}`
|
|
515
|
-
: "Tunnel enable requested.";
|
|
516
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
517
|
-
} catch (error) {
|
|
518
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
519
|
-
return { code: 1, stdout: "", stderr: message + "\n" };
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (command === "daemon:disable-tunnel") {
|
|
524
|
-
try {
|
|
525
|
-
const { disableDaemonTunnel } = await import('./daemon/launcher.d.ts');
|
|
526
|
-
const status = await disableDaemonTunnel({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
527
|
-
const body = flags.json
|
|
528
|
-
? JSON.stringify({ ok: true, ...serializeDaemonStatus(status) })
|
|
529
|
-
: "Tunnel disabled.";
|
|
530
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
531
|
-
} catch (error) {
|
|
532
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
533
|
-
return { code: 1, stdout: "", stderr: message + "\n" };
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (command === "daemon:list-providers") {
|
|
538
|
-
const providersModule = await import('./daemon/tunnel-providers/index.d.ts');
|
|
539
|
-
const configDir = process.env.PERPLEXITY_CONFIG_DIR;
|
|
540
|
-
const statuses = await providersModule.listTunnelProviderStatuses(configDir);
|
|
541
|
-
const active = providersModule.readTunnelSettings(configDir).activeProvider;
|
|
542
|
-
const body = flags.json
|
|
543
|
-
? JSON.stringify({ active, providers: statuses })
|
|
544
|
-
: statuses
|
|
545
|
-
.map((s) => `${s.isActive ? "*" : " "} ${s.id.padEnd(10)} ${s.displayName.padEnd(22)} ${s.setup.ready ? "ready" : s.setup.reason ?? "needs setup"}`)
|
|
546
|
-
.join("\n");
|
|
547
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (command === "daemon:set-provider") {
|
|
551
|
-
const providerId = parsed.positional?.[0];
|
|
552
|
-
if (!providerId) {
|
|
553
|
-
return { code: 1, stdout: "", stderr: "set-provider requires a provider id (cf-quick | ngrok | cf-named).\n" };
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
const providersModule = await import('./daemon/tunnel-providers/index.d.ts');
|
|
557
|
-
const configDir = process.env.PERPLEXITY_CONFIG_DIR;
|
|
558
|
-
providersModule.writeTunnelSettings(configDir, { activeProvider: providerId });
|
|
559
|
-
return { code: 0, stdout: `Active tunnel provider set to ${providerId}.\n`, stderr: "" };
|
|
560
|
-
} catch (error) {
|
|
561
|
-
return { code: 1, stdout: "", stderr: (error instanceof Error ? error.message : String(error)) + "\n" };
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (command === "daemon:set-ngrok-authtoken") {
|
|
566
|
-
const authtoken = parsed.positional?.[0] ?? flags.token;
|
|
567
|
-
if (!authtoken || typeof authtoken !== "string" || authtoken.length < 10) {
|
|
568
|
-
return { code: 1, stdout: "", stderr: "set-ngrok-authtoken requires an authtoken (see dashboard.ngrok.com/get-started/your-authtoken).\n" };
|
|
569
|
-
}
|
|
570
|
-
try {
|
|
571
|
-
const providersModule = await import('./daemon/tunnel-providers/index.d.ts');
|
|
572
|
-
providersModule.writeNgrokSettings(process.env.PERPLEXITY_CONFIG_DIR, { authtoken });
|
|
573
|
-
return { code: 0, stdout: "ngrok authtoken saved.\n", stderr: "" };
|
|
574
|
-
} catch (error) {
|
|
575
|
-
return { code: 1, stdout: "", stderr: (error instanceof Error ? error.message : String(error)) + "\n" };
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (command === "daemon:set-ngrok-domain") {
|
|
580
|
-
const domain = parsed.positional?.[0] ?? flags.domain ?? null;
|
|
581
|
-
try {
|
|
582
|
-
const providersModule = await import('./daemon/tunnel-providers/index.d.ts');
|
|
583
|
-
providersModule.writeNgrokSettings(process.env.PERPLEXITY_CONFIG_DIR, { domain: domain ?? null });
|
|
584
|
-
return { code: 0, stdout: (domain ? `ngrok domain set to ${domain}.\n` : "ngrok domain cleared.\n"), stderr: "" };
|
|
585
|
-
} catch (error) {
|
|
586
|
-
return { code: 1, stdout: "", stderr: (error instanceof Error ? error.message : String(error)) + "\n" };
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (command === "daemon:clear-ngrok") {
|
|
591
|
-
try {
|
|
592
|
-
const providersModule = await import('./daemon/tunnel-providers/index.d.ts');
|
|
593
|
-
providersModule.clearNgrokSettings(process.env.PERPLEXITY_CONFIG_DIR);
|
|
594
|
-
return { code: 0, stdout: "ngrok settings cleared.\n", stderr: "" };
|
|
595
|
-
} catch (error) {
|
|
596
|
-
return { code: 1, stdout: "", stderr: (error instanceof Error ? error.message : String(error)) + "\n" };
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
601
|
-
// cf-named (Cloudflare Named Tunnel) CLI — mirrors the 8.4.3 dashboard
|
|
602
|
-
// widget for npm-only users. Helpers imported directly from the
|
|
603
|
-
// mcp-server; do NOT import the extension's runtime.ts (VS Code-private).
|
|
604
|
-
//
|
|
605
|
-
// Dashed subcommand names (daemon cf-named-login, etc.) so the existing
|
|
606
|
-
// parseArgs one-level-deep routing (daemon <x> → daemon:<x>) works
|
|
607
|
-
// unchanged. Documented identically in DAEMON_HELP_TEXT.
|
|
608
|
-
//
|
|
609
|
-
// Login, create, bind each modal-confirm via stderr/stdin unless --yes.
|
|
610
|
-
// Exit 130 on user decline (standard "interrupted by user" code).
|
|
611
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
612
|
-
|
|
613
|
-
if (command === "daemon:cf-named-login") {
|
|
614
|
-
if (!flags.yes) {
|
|
615
|
-
const { promptYesNo } = await import('./tty-prompt.d.ts');
|
|
616
|
-
const ok = await promptYesNo({
|
|
617
|
-
prompt: "This opens your default browser to authorize Cloudflare. Continue? [y/N] ",
|
|
618
|
-
});
|
|
619
|
-
if (!ok) {
|
|
620
|
-
return { code: 130, stdout: "", stderr: "Cancelled.\n" };
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
try {
|
|
624
|
-
const { runCloudflaredLogin } = await import('./daemon/tunnel-providers/index.d.ts');
|
|
625
|
-
// forwardOutput: pipe cloudflared's child stderr AND stdout to OUR
|
|
626
|
-
// stderr so the CLI user sees the "open this URL in your browser"
|
|
627
|
-
// prompt. Never to our stdout — that's reserved for --json payload.
|
|
628
|
-
const result = await runCloudflaredLogin({
|
|
629
|
-
configDir: process.env.PERPLEXITY_CONFIG_DIR,
|
|
630
|
-
forwardOutput: true,
|
|
631
|
-
});
|
|
632
|
-
const body = flags.json
|
|
633
|
-
? JSON.stringify({ ok: true, certPath: result.certPath })
|
|
634
|
-
: `cloudflared login completed. Cert at ${result.certPath}`;
|
|
635
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
636
|
-
} catch (error) {
|
|
637
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
638
|
-
const hint = /not installed/i.test(msg)
|
|
639
|
-
? `${msg}\nRun 'npx perplexity-user-mcp daemon install-tunnel' to install cloudflared.\n`
|
|
640
|
-
: msg + "\n";
|
|
641
|
-
return { code: 1, stdout: "", stderr: hint };
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (command === "daemon:cf-named-list") {
|
|
646
|
-
try {
|
|
647
|
-
const { listNamedTunnels } = await import('./daemon/tunnel-providers/index.d.ts');
|
|
648
|
-
const tunnels = await listNamedTunnels({ configDir: process.env.PERPLEXITY_CONFIG_DIR });
|
|
649
|
-
const body = flags.json
|
|
650
|
-
? JSON.stringify({ tunnels })
|
|
651
|
-
: tunnels.length === 0
|
|
652
|
-
? "No named tunnels."
|
|
653
|
-
: tunnels
|
|
654
|
-
.map((t) => `${t.uuid} ${t.name} (${t.connections ?? 0} connections)`)
|
|
655
|
-
.join("\n");
|
|
656
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
657
|
-
} catch (error) {
|
|
658
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
659
|
-
return { code: 1, stdout: "", stderr: msg + "\n" };
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (command === "daemon:cf-named-create") {
|
|
664
|
-
const name = flags.name ?? parsed.positional?.[0];
|
|
665
|
-
const hostname = flags.hostname ?? parsed.positional?.[1];
|
|
666
|
-
if (!name || typeof name !== "string") {
|
|
667
|
-
return { code: 1, stdout: "", stderr: "cf-named-create requires --name (or first positional argument).\n" };
|
|
668
|
-
}
|
|
669
|
-
if (!hostname || typeof hostname !== "string") {
|
|
670
|
-
return { code: 1, stdout: "", stderr: "cf-named-create requires --hostname (or second positional argument).\n" };
|
|
671
|
-
}
|
|
672
|
-
if (!flags.yes) {
|
|
673
|
-
const { promptYesNo } = await import('./tty-prompt.d.ts');
|
|
674
|
-
const ok = await promptYesNo({
|
|
675
|
-
prompt: `This creates a Cloudflare tunnel "${name}" and routes DNS "${hostname}" under your zone. Continue? [y/N] `,
|
|
676
|
-
});
|
|
677
|
-
if (!ok) {
|
|
678
|
-
return { code: 130, stdout: "", stderr: "Cancelled.\n" };
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
const { createNamedTunnel, writeTunnelConfig } = await import('./daemon/tunnel-providers/index.d.ts');
|
|
683
|
-
const configDir = process.env.PERPLEXITY_CONFIG_DIR;
|
|
684
|
-
const created = await createNamedTunnel({ configDir, name, hostname });
|
|
685
|
-
// Placeholder port=1; the cf-named provider's start() rewrites it to the
|
|
686
|
-
// live daemon port on every spawn (port-drift rewrite), so this value is
|
|
687
|
-
// never read in practice. Matches the 8.4.3 dashboard behavior.
|
|
688
|
-
const config = writeTunnelConfig({
|
|
689
|
-
configDir,
|
|
690
|
-
uuid: created.uuid,
|
|
691
|
-
hostname,
|
|
692
|
-
port: 1,
|
|
693
|
-
credentialsPath: created.credentialsPath,
|
|
694
|
-
});
|
|
695
|
-
const body = flags.json
|
|
696
|
-
? JSON.stringify({ ok: true, uuid: created.uuid, name: created.name, hostname, configPath: config.configPath, credentialsPath: created.credentialsPath })
|
|
697
|
-
: `Tunnel created: uuid=${created.uuid} hostname=${hostname}\nConfig written to ${config.configPath}`;
|
|
698
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
699
|
-
} catch (error) {
|
|
700
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
701
|
-
return { code: 1, stdout: "", stderr: msg + "\n" };
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (command === "daemon:cf-named-bind") {
|
|
706
|
-
const uuid = flags.uuid ?? parsed.positional?.[0];
|
|
707
|
-
const hostname = flags.hostname ?? parsed.positional?.[1];
|
|
708
|
-
if (!uuid || typeof uuid !== "string") {
|
|
709
|
-
return { code: 1, stdout: "", stderr: "cf-named-bind requires --uuid (or first positional argument).\n" };
|
|
710
|
-
}
|
|
711
|
-
if (!hostname || typeof hostname !== "string") {
|
|
712
|
-
return { code: 1, stdout: "", stderr: "cf-named-bind requires --hostname (or second positional argument).\n" };
|
|
713
|
-
}
|
|
714
|
-
const credentialsPath = join(homedir(), ".cloudflared", `${uuid}.json`);
|
|
715
|
-
if (!existsSync(credentialsPath)) {
|
|
716
|
-
return {
|
|
717
|
-
code: 1,
|
|
718
|
-
stdout: "",
|
|
719
|
-
stderr: `Credentials file not found at ${credentialsPath}. Run 'cloudflared tunnel create' for this UUID first, or use 'cf-named-create'.\n`,
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
if (!flags.yes) {
|
|
723
|
-
const { promptYesNo } = await import('./tty-prompt.d.ts');
|
|
724
|
-
const ok = await promptYesNo({
|
|
725
|
-
prompt: `This writes a managed config binding tunnel ${uuid} to ${hostname}. Continue? [y/N] `,
|
|
726
|
-
});
|
|
727
|
-
if (!ok) {
|
|
728
|
-
return { code: 130, stdout: "", stderr: "Cancelled.\n" };
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
try {
|
|
732
|
-
const { writeTunnelConfig } = await import('./daemon/tunnel-providers/index.d.ts');
|
|
733
|
-
const configDir = process.env.PERPLEXITY_CONFIG_DIR;
|
|
734
|
-
const config = writeTunnelConfig({
|
|
735
|
-
configDir,
|
|
736
|
-
uuid,
|
|
737
|
-
hostname,
|
|
738
|
-
port: 1,
|
|
739
|
-
credentialsPath,
|
|
740
|
-
});
|
|
741
|
-
const body = flags.json
|
|
742
|
-
? JSON.stringify({ ok: true, uuid, hostname, configPath: config.configPath, credentialsPath })
|
|
743
|
-
: `Bound tunnel ${uuid} to ${hostname}.\nConfig written to ${config.configPath}`;
|
|
744
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
745
|
-
} catch (error) {
|
|
746
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
747
|
-
return { code: 1, stdout: "", stderr: msg + "\n" };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (command === "list-accounts") {
|
|
752
|
-
const { listProfiles, getActiveName } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
753
|
-
const profiles = listProfiles();
|
|
754
|
-
const active = getActiveName();
|
|
755
|
-
const body = flags.json
|
|
756
|
-
? JSON.stringify({ ok: true, active, profiles })
|
|
757
|
-
: profiles.length === 0
|
|
758
|
-
? "No profiles yet. Run `add-account` to create one."
|
|
759
|
-
: profiles.map((p) => `${p.name === active ? "* " : " "}${p.name} [${p.tier ?? "?"}] mode=${p.loginMode ?? "?"} lastLogin=${p.lastLogin ?? "never"}`).join("\n");
|
|
760
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
761
|
-
}
|
|
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
|
-
|
|
787
|
-
if (command === "add-account") {
|
|
788
|
-
const name = flags.name ?? (await import('./profiles.d-DqS1oZWr.d.ts')).suggestNextDefaultName();
|
|
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
|
-
|
|
807
|
-
try {
|
|
808
|
-
const { createProfile } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
809
|
-
const profile = createProfile(name, { loginMode: mode });
|
|
810
|
-
const body = flags.json ? JSON.stringify({ ok: true, profile }) : `Created profile '${name}'.`;
|
|
811
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
812
|
-
} catch (err) {
|
|
813
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
814
|
-
return { code: 1, stdout: flags.json ? JSON.stringify({ ok: false, error: msg }) + "\n" : "", stderr: msg + "\n" };
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if (command === "switch-account") {
|
|
819
|
-
const target = parsed.positional?.[0];
|
|
820
|
-
if (!target) return { code: 1, stdout: "", stderr: "switch-account requires a profile name.\n" };
|
|
821
|
-
try {
|
|
822
|
-
const { setActive } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
823
|
-
setActive(target);
|
|
824
|
-
const body = flags.json ? JSON.stringify({ ok: true, active: target }) : `Switched to '${target}'.`;
|
|
825
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
826
|
-
} catch (err) {
|
|
827
|
-
return { code: 1, stdout: "", stderr: `${err.message}\n` };
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (command === "logout") {
|
|
832
|
-
const { softLogout, hardLogout } = await import('./logout.d.ts');
|
|
833
|
-
const name = flags.profile ?? (await import('./profiles.d-DqS1oZWr.d.ts')).getActiveName() ?? "default";
|
|
834
|
-
if (flags.purge) await hardLogout(name); else await softLogout(name);
|
|
835
|
-
const body = flags.json ? JSON.stringify({ ok: true, purged: !!flags.purge, profile: name }) : `Logged out of '${name}'.`;
|
|
836
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
if (command === "status") {
|
|
840
|
-
const name = flags.profile ?? (await import('./profiles.d-DqS1oZWr.d.ts')).getActiveName() ?? "default";
|
|
841
|
-
const { Vault } = await import('./vault.d-BSJWDLhp.d.ts');
|
|
842
|
-
/* v8 ignore next -- defensive catch for unreadable vault (malformed blob, wrong key) */
|
|
843
|
-
const cookies = await new Vault().get(name, "cookies").catch(() => null);
|
|
844
|
-
if (!cookies) {
|
|
845
|
-
const body = flags.json ? JSON.stringify({ valid: false, reason: "no_cookies", profile: name }) : `No session for '${name}'. Run login first.`;
|
|
846
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
847
|
-
}
|
|
848
|
-
const { getProfile } = await import('./profiles.d-DqS1oZWr.d.ts');
|
|
849
|
-
const meta = getProfile(name);
|
|
850
|
-
const body = flags.json
|
|
851
|
-
? JSON.stringify({ valid: true, profile: name, tier: meta?.tier, lastLogin: meta?.lastLogin })
|
|
852
|
-
: `Profile '${name}' has stored cookies. Tier=${meta?.tier ?? "?"} lastLogin=${meta?.lastLogin ?? "?"}`;
|
|
853
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/* v8 ignore start -- login spawns a long-lived fork with a real browser; covered by integration suites */
|
|
857
|
-
if (command === "login") {
|
|
858
|
-
const { fork } = await import('node:child_process');
|
|
859
|
-
const mode = flags.mode ?? "manual";
|
|
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
|
-
|
|
877
|
-
const env = { ...process.env, PERPLEXITY_PROFILE: profile };
|
|
878
|
-
if (mode === "auto") {
|
|
879
|
-
if (!flags.email) return { code: 1, stdout: "", stderr: "`--email` required for --mode auto.\n" };
|
|
880
|
-
env.PERPLEXITY_EMAIL = String(flags.email);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Auto-enable when impit (Speed Boost) is installed — the install is
|
|
884
|
-
// the opt-in. `--no-impit` or PERPLEXITY_DISABLE_IMPIT_LOGIN=1 forces
|
|
885
|
-
// the browser path. Falls back to the browser-based runner on impit-
|
|
886
|
-
// only failures (cf_blocked, impit_missing, crash).
|
|
887
|
-
const wantImpit =
|
|
888
|
-
mode === "auto" &&
|
|
889
|
-
!flags["no-impit"] &&
|
|
890
|
-
process.env.PERPLEXITY_DISABLE_IMPIT_LOGIN !== "1" &&
|
|
891
|
-
(await import('./refresh.d.ts')).isImpitAvailable();
|
|
892
|
-
|
|
893
|
-
const browserRunnerName = mode === "auto" ? "./login-runner.mjs" : "./manual-login-runner.mjs";
|
|
894
|
-
const browserRunner = fileURLToPath(new URL(browserRunnerName, import.meta.url));
|
|
895
|
-
const impitRunner = fileURLToPath(new URL("./impit-login-runner.mjs", import.meta.url));
|
|
896
|
-
const IMPIT_FALLBACK_REASONS = new Set(["cf_blocked", "impit_missing", "impit_load_failed", "auto_unsupported", "crash"]);
|
|
897
|
-
|
|
898
|
-
async function spawnRunner(runner) {
|
|
899
|
-
return new Promise((resolve) => {
|
|
900
|
-
const child = fork(runner, [], { env, stdio: ["inherit", "pipe", "inherit", "ipc"] });
|
|
901
|
-
let out = "";
|
|
902
|
-
child.stdout.on("data", (d) => { out += d.toString(); process.stderr.write(d); });
|
|
903
|
-
child.on("message", async (m) => {
|
|
904
|
-
if (m?.phase === "awaiting_otp") {
|
|
905
|
-
const { promptSecret } = await import('./tty-prompt.d.ts');
|
|
906
|
-
const otp = await promptSecret({ prompt: "Enter OTP from your email: " });
|
|
907
|
-
child.send({ otp });
|
|
908
|
-
}
|
|
909
|
-
});
|
|
910
|
-
child.on("close", (code) => {
|
|
911
|
-
const lines = out.trim().split("\n").filter(Boolean);
|
|
912
|
-
const last = lines[lines.length - 1];
|
|
913
|
-
let parsed = null;
|
|
914
|
-
try { parsed = last ? JSON.parse(last) : null; } catch { /* not JSON */ }
|
|
915
|
-
resolve({ code: code ?? 0, last, parsed });
|
|
916
|
-
});
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (wantImpit) {
|
|
921
|
-
const impitResult = await spawnRunner(impitRunner);
|
|
922
|
-
const reason = impitResult.parsed?.reason;
|
|
923
|
-
const ok = impitResult.parsed?.ok === true;
|
|
924
|
-
if (ok || (reason && !IMPIT_FALLBACK_REASONS.has(reason))) {
|
|
925
|
-
return { code: impitResult.code, stdout: (flags.json ? impitResult.last : `login finished (${impitResult.code})`) + "\n", stderr: "" };
|
|
926
|
-
}
|
|
927
|
-
process.stderr.write(`[cli login] impit runner failed (${reason ?? "unknown"}); falling back to browser.\n`);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const browserResult = await spawnRunner(browserRunner);
|
|
931
|
-
return { code: browserResult.code, stdout: (flags.json ? browserResult.last : `login finished (${browserResult.code})`) + "\n", stderr: "" };
|
|
932
|
-
}
|
|
933
|
-
/* v8 ignore stop */
|
|
934
|
-
|
|
935
|
-
if (command === "install-speed-boost") {
|
|
936
|
-
const { installImpit, getImpitStatus } = await import('./native-deps-BNThFHxa.d.ts');
|
|
937
|
-
const before = getImpitStatus();
|
|
938
|
-
if (before.installed && !flags.force) {
|
|
939
|
-
const msg = flags.json
|
|
940
|
-
? JSON.stringify({ ok: true, alreadyInstalled: true, version: before.version, runtimeDir: before.runtimeDir })
|
|
941
|
-
: `Speed Boost (impit ${before.version ?? "?"}) already installed at ${before.runtimeDir}.\nPass --force to reinstall.`;
|
|
942
|
-
return { code: 0, stdout: msg + "\n", stderr: "" };
|
|
943
|
-
}
|
|
944
|
-
const log = (line) => process.stderr.write(`[speed-boost] ${line}\n`);
|
|
945
|
-
const result = await installImpit({ log });
|
|
946
|
-
if (!result.ok) {
|
|
947
|
-
const stderr = flags.json
|
|
948
|
-
? JSON.stringify({ ok: false, error: result.error }) + "\n"
|
|
949
|
-
: `Speed Boost install failed: ${result.error}\n`;
|
|
950
|
-
return { code: 1, stdout: "", stderr };
|
|
951
|
-
}
|
|
952
|
-
const status = getImpitStatus();
|
|
953
|
-
const out = flags.json
|
|
954
|
-
? JSON.stringify({ ok: true, version: status.version, installedAt: status.installedAt, runtimeDir: status.runtimeDir })
|
|
955
|
-
: `Speed Boost installed: impit ${status.version ?? "?"} at ${status.runtimeDir}.\nAll impit-eligible tools (sync, hydrate, retrieve, export, models, login) will use it automatically.`;
|
|
956
|
-
return { code: 0, stdout: out + "\n", stderr: "" };
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
if (command === "uninstall-speed-boost") {
|
|
960
|
-
const { uninstallImpit, getImpitStatus } = await import('./native-deps-BNThFHxa.d.ts');
|
|
961
|
-
const before = getImpitStatus();
|
|
962
|
-
const log = (line) => process.stderr.write(`[speed-boost] ${line}\n`);
|
|
963
|
-
const result = uninstallImpit({ log });
|
|
964
|
-
if (!result.ok) {
|
|
965
|
-
const stderr = flags.json
|
|
966
|
-
? JSON.stringify({ ok: false, error: result.error }) + "\n"
|
|
967
|
-
: `Speed Boost uninstall failed: ${result.error}\n`;
|
|
968
|
-
return { code: 1, stdout: "", stderr };
|
|
969
|
-
}
|
|
970
|
-
const out = flags.json
|
|
971
|
-
? JSON.stringify({ ok: true, hadImpit: before.installed })
|
|
972
|
-
: before.installed
|
|
973
|
-
? `Speed Boost removed (was impit ${before.version ?? "?"}). Affected tools fall back to the browser path.`
|
|
974
|
-
: `Speed Boost was not installed. Nothing to remove.`;
|
|
975
|
-
return { code: 0, stdout: out + "\n", stderr: "" };
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
if (command === "speed-boost-status") {
|
|
979
|
-
const { getImpitStatus } = await import('./native-deps-BNThFHxa.d.ts');
|
|
980
|
-
const status = getImpitStatus();
|
|
981
|
-
if (flags.json) {
|
|
982
|
-
return { code: 0, stdout: JSON.stringify(status) + "\n", stderr: "" };
|
|
983
|
-
}
|
|
984
|
-
const out = status.installed
|
|
985
|
-
? `Speed Boost: installed (impit ${status.version ?? "?"}${status.installedAt ? `, installed ${status.installedAt}` : ""}).\nRuntime dir: ${status.runtimeDir}`
|
|
986
|
-
: `Speed Boost: not installed.\nRun: npx perplexity-user-mcp install-speed-boost\nRuntime dir (for manual install): ${status.runtimeDir}`;
|
|
987
|
-
return { code: 0, stdout: out + "\n", stderr: "" };
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (command === "doctor") {
|
|
991
|
-
const { runAll, exitCodeFor, formatReportMarkdown } = await import('./doctor.d-CXmUqOXX.d.ts');
|
|
992
|
-
const report = await runAll({
|
|
993
|
-
profile: flags.profile,
|
|
994
|
-
probe: !!flags.probe,
|
|
995
|
-
allProfiles: !!flags.all,
|
|
996
|
-
});
|
|
997
|
-
const exit = exitCodeFor(report);
|
|
998
|
-
if (flags.json) {
|
|
999
|
-
return { code: exit, stdout: JSON.stringify(report) + "\n", stderr: "" };
|
|
1000
|
-
}
|
|
1001
|
-
return { code: exit, stdout: formatReportMarkdown(report) + "\n", stderr: "" };
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (command === "export") {
|
|
1005
|
-
const historyId = parsed.positional?.[0];
|
|
1006
|
-
if (!historyId) return { code: 1, stdout: "", stderr: "export requires a history id.\n" };
|
|
1007
|
-
|
|
1008
|
-
const format = normalizeExportFormat(flags.format);
|
|
1009
|
-
if (!format) return { code: 1, stdout: "", stderr: "export requires --format pdf|md|markdown|docx.\n" };
|
|
1010
|
-
|
|
1011
|
-
const { get } = await import('./history-store.d-BzjBF2m3.d.ts');
|
|
1012
|
-
const entry = get(historyId);
|
|
1013
|
-
if (!entry) return { code: 1, stdout: "", stderr: `History entry '${historyId}' not found.\n` };
|
|
1014
|
-
|
|
1015
|
-
if (format === "markdown") {
|
|
1016
|
-
const targetPath = flags.out ? String(flags.out) : join(entry.attachmentsDir, entry.mdPath.split(/[\\/]/).pop() || `${entry.id}.md`);
|
|
1017
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1018
|
-
writeFileSync(targetPath, readFileSync(entry.mdPath, "utf8"), "utf8");
|
|
1019
|
-
const body = flags.json
|
|
1020
|
-
? JSON.stringify({ ok: true, format, savedPath: targetPath, historyId })
|
|
1021
|
-
: `Saved markdown export to ${targetPath}`;
|
|
1022
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (!entry.threadSlug) {
|
|
1026
|
-
return { code: 1, stdout: "", stderr: "This entry cannot be exported natively because it has no Perplexity thread slug.\n" };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const { PerplexityClient } = await import('./client.d.ts');
|
|
1030
|
-
const client = new PerplexityClient();
|
|
1031
|
-
try {
|
|
1032
|
-
await client.init();
|
|
1033
|
-
const exported = await client.exportThread({ threadSlug: entry.threadSlug, format });
|
|
1034
|
-
const targetPath = flags.out ? String(flags.out) : join(entry.attachmentsDir, exported.filename);
|
|
1035
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1036
|
-
writeFileSync(targetPath, exported.buffer);
|
|
1037
|
-
const body = flags.json
|
|
1038
|
-
? JSON.stringify({ ok: true, format, savedPath: targetPath, bytes: exported.buffer.length, contentType: exported.contentType, historyId })
|
|
1039
|
-
: `Saved ${format} export to ${targetPath}`;
|
|
1040
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
1041
|
-
} finally {
|
|
1042
|
-
await client.shutdown().catch(() => undefined);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (command === "sync-cloud") {
|
|
1047
|
-
const previousProfile = process.env.PERPLEXITY_PROFILE;
|
|
1048
|
-
try {
|
|
1049
|
-
if (flags.profile) process.env.PERPLEXITY_PROFILE = String(flags.profile);
|
|
1050
|
-
const { syncCloudHistory } = await import('./cloud-sync.d-Cqt6y18U.d.ts');
|
|
1051
|
-
const pageSize = flags["page-size"] !== undefined ? Number(flags["page-size"]) : undefined;
|
|
1052
|
-
const lines = [];
|
|
1053
|
-
const result = await syncCloudHistory({
|
|
1054
|
-
pageSize: Number.isFinite(pageSize) && pageSize > 0 ? pageSize : undefined,
|
|
1055
|
-
onProgress: (evt) => {
|
|
1056
|
-
if (flags.verbose) lines.push(`[sync] ${evt.phase} fetched=${evt.fetched ?? 0} inserted=${evt.inserted ?? 0} updated=${evt.updated ?? 0} skipped=${evt.skipped ?? 0}`);
|
|
1057
|
-
},
|
|
1058
|
-
});
|
|
1059
|
-
const body = flags.json
|
|
1060
|
-
? JSON.stringify(result)
|
|
1061
|
-
: `Cloud sync: fetched=${result.fetched} inserted=${result.inserted} updated=${result.updated} skipped=${result.skipped}`;
|
|
1062
|
-
return { code: 0, stdout: body + "\n", stderr: flags.verbose ? lines.join("\n") + "\n" : "" };
|
|
1063
|
-
} catch (err) {
|
|
1064
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1065
|
-
return { code: 10, stdout: "", stderr: `Cloud sync failed: ${message}\n` };
|
|
1066
|
-
} finally {
|
|
1067
|
-
if (flags.profile === undefined && previousProfile !== undefined) {
|
|
1068
|
-
process.env.PERPLEXITY_PROFILE = previousProfile;
|
|
1069
|
-
} else if (previousProfile === undefined) {
|
|
1070
|
-
delete process.env.PERPLEXITY_PROFILE;
|
|
1071
|
-
} else {
|
|
1072
|
-
process.env.PERPLEXITY_PROFILE = previousProfile;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
if (command === "rebuild-history-index") {
|
|
1078
|
-
const previousProfile = process.env.PERPLEXITY_PROFILE;
|
|
1079
|
-
try {
|
|
1080
|
-
if (flags.profile) {
|
|
1081
|
-
process.env.PERPLEXITY_PROFILE = String(flags.profile);
|
|
1082
|
-
}
|
|
1083
|
-
const { rebuildIndex } = await import('./history-store.d-BzjBF2m3.d.ts');
|
|
1084
|
-
const result = rebuildIndex();
|
|
1085
|
-
const body = flags.json
|
|
1086
|
-
? JSON.stringify(result)
|
|
1087
|
-
: `Rebuilt history index: scanned=${result.scanned} recovered=${result.recovered} skipped=${result.skipped}`;
|
|
1088
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
1089
|
-
} finally {
|
|
1090
|
-
if (flags.profile === undefined && previousProfile !== undefined) {
|
|
1091
|
-
process.env.PERPLEXITY_PROFILE = previousProfile;
|
|
1092
|
-
} else if (previousProfile === undefined) {
|
|
1093
|
-
delete process.env.PERPLEXITY_PROFILE;
|
|
1094
|
-
} else {
|
|
1095
|
-
process.env.PERPLEXITY_PROFILE = previousProfile;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
if (command === "open") {
|
|
1101
|
-
const historyId = parsed.positional?.[0];
|
|
1102
|
-
if (!historyId) return { code: 1, stdout: "", stderr: "open requires a history id.\n" };
|
|
1103
|
-
|
|
1104
|
-
const { get } = await import('./history-store.d-BzjBF2m3.d.ts');
|
|
1105
|
-
const entry = get(historyId);
|
|
1106
|
-
if (!entry) return { code: 1, stdout: "", stderr: `History entry '${historyId}' not found.\n` };
|
|
1107
|
-
|
|
1108
|
-
const viewerId = String(flags.viewer ?? "system");
|
|
1109
|
-
let target = entry.mdPath;
|
|
1110
|
-
|
|
1111
|
-
if (viewerId !== "system") {
|
|
1112
|
-
const { buildViewerUrl, listViewers } = await import('./viewers.d-BGCK6sw6.d.ts');
|
|
1113
|
-
const viewer = listViewers().find((item) => item.id === viewerId);
|
|
1114
|
-
if (!viewer) {
|
|
1115
|
-
return { code: 1, stdout: "", stderr: `Unknown viewer '${viewerId}'.\n` };
|
|
1116
|
-
}
|
|
1117
|
-
target = buildViewerUrl({ viewer, mdPath: entry.mdPath });
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
await openTarget(target);
|
|
1121
|
-
const body = flags.json
|
|
1122
|
-
? JSON.stringify({ ok: true, viewer: viewerId, target, historyId })
|
|
1123
|
-
: `Opened ${historyId} via ${viewerId}: ${target}`;
|
|
1124
|
-
return { code: 0, stdout: body + "\n", stderr: "" };
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Phase-1 stub: all real subcommands are placeholder until their phases land.
|
|
1128
|
-
const msg = flags.json
|
|
1129
|
-
? JSON.stringify({ ok: false, error: "not-yet-implemented", command })
|
|
1130
|
-
: `'${command}' is not yet implemented (arrives in Phase ${phaseFor(command)}).`;
|
|
1131
|
-
return { code: 0, stdout: msg + "\n", stderr: "" };
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function phaseFor(cmd) {
|
|
1135
|
-
if (cmd === "install-browser") return 3;
|
|
1136
|
-
if (cmd === "export" || cmd === "open" || cmd === "rebuild-history-index" || cmd === "sync-cloud") return 4;
|
|
1137
|
-
/* v8 ignore next -- fallback for unmapped commands that shouldn't exist */
|
|
1138
|
-
return "?";
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function parseOptionalPort(value) {
|
|
1142
|
-
if (value === undefined || value === true) return null;
|
|
1143
|
-
const parsed = Number(value);
|
|
1144
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
return parsed;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
function formatDaemonStatus(status) {
|
|
1151
|
-
if (!status.running || !status.record) {
|
|
1152
|
-
return "Daemon is not running.";
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
if (!status.healthy || !status.health) {
|
|
1156
|
-
return `Daemon lock exists for pid=${status.record.pid}, but the health probe is not ready.`;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const tunnelUrl = status.health.tunnel?.url ?? status.record.tunnelUrl ?? null;
|
|
1160
|
-
const parts = [
|
|
1161
|
-
`Daemon running pid=${status.record.pid} port=${status.record.port}`,
|
|
1162
|
-
`uptime=${formatDuration(status.health.uptimeMs)}`,
|
|
1163
|
-
];
|
|
1164
|
-
if (tunnelUrl) {
|
|
1165
|
-
parts.push(`tunnel=${tunnelUrl}`);
|
|
1166
|
-
}
|
|
1167
|
-
return parts.join(" ");
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function serializeDaemonStatus(status) {
|
|
1171
|
-
return {
|
|
1172
|
-
running: status.running,
|
|
1173
|
-
healthy: status.healthy,
|
|
1174
|
-
stale: status.stale,
|
|
1175
|
-
pid: status.record?.pid ?? null,
|
|
1176
|
-
uuid: status.record?.uuid ?? null,
|
|
1177
|
-
port: status.record?.port ?? null,
|
|
1178
|
-
version: status.record?.version ?? null,
|
|
1179
|
-
startedAt: status.record?.startedAt ?? null,
|
|
1180
|
-
tunnelUrl: status.health?.tunnel?.url ?? status.record?.tunnelUrl ?? null,
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function serializeDaemonConnection(daemon) {
|
|
1185
|
-
return {
|
|
1186
|
-
pid: daemon.pid,
|
|
1187
|
-
uuid: daemon.uuid,
|
|
1188
|
-
port: daemon.port,
|
|
1189
|
-
url: daemon.url,
|
|
1190
|
-
version: daemon.version,
|
|
1191
|
-
startedAt: daemon.startedAt,
|
|
1192
|
-
tunnelUrl: daemon.tunnelUrl ?? null,
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
function formatDuration(durationMs) {
|
|
1197
|
-
if (!Number.isFinite(durationMs) || durationMs < 0) {
|
|
1198
|
-
return "0s";
|
|
1199
|
-
}
|
|
1200
|
-
const seconds = Math.floor(durationMs / 1000);
|
|
1201
|
-
if (seconds < 60) return `${seconds}s`;
|
|
1202
|
-
const minutes = Math.floor(seconds / 60);
|
|
1203
|
-
const remainder = seconds % 60;
|
|
1204
|
-
if (minutes < 60) return `${minutes}m${remainder}s`;
|
|
1205
|
-
const hours = Math.floor(minutes / 60);
|
|
1206
|
-
return `${hours}h${minutes % 60}m`;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const HELP_TEXT = `perplexity-user-mcp
|
|
1210
|
-
|
|
1211
|
-
Usage:
|
|
1212
|
-
npx perplexity-user-mcp Start MCP stdio server
|
|
1213
|
-
npx perplexity-user-mcp daemon start [--port N] [--tunnel]
|
|
1214
|
-
npx perplexity-user-mcp daemon stop
|
|
1215
|
-
npx perplexity-user-mcp daemon status [--json]
|
|
1216
|
-
npx perplexity-user-mcp daemon attach [--fallback-stdio] [--ensure-timeout-ms N]
|
|
1217
|
-
npx perplexity-user-mcp daemon rotate-token
|
|
1218
|
-
npx perplexity-user-mcp login [--profile X] [--mode auto|manual] [--plain-cookies]
|
|
1219
|
-
npx perplexity-user-mcp logout [--profile X] [--purge]
|
|
1220
|
-
npx perplexity-user-mcp status [--profile X] [--all]
|
|
1221
|
-
npx perplexity-user-mcp doctor [--profile X] [--probe] [--all] [--report]
|
|
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.
|
|
1230
|
-
npx perplexity-user-mcp install-speed-boost [--force] [--json]
|
|
1231
|
-
npx perplexity-user-mcp uninstall-speed-boost [--json]
|
|
1232
|
-
npx perplexity-user-mcp speed-boost-status [--json]
|
|
1233
|
-
npx perplexity-user-mcp add-account [--name X] [--email Y] [--mode auto|manual] [--plain-cookies]
|
|
1234
|
-
npx perplexity-user-mcp switch-account <name>
|
|
1235
|
-
npx perplexity-user-mcp list-accounts
|
|
1236
|
-
npx perplexity-user-mcp export <id> --format pdf|md|docx [--out path]
|
|
1237
|
-
npx perplexity-user-mcp open <id> [--viewer obsidian|typora|logseq|system]
|
|
1238
|
-
npx perplexity-user-mcp rebuild-history-index [--profile X]
|
|
1239
|
-
npx perplexity-user-mcp sync-cloud [--profile X] [--page-size N] [--verbose]
|
|
1240
|
-
npx perplexity-user-mcp --version
|
|
1241
|
-
npx perplexity-user-mcp --help
|
|
1242
|
-
|
|
1243
|
-
Environment:
|
|
1244
|
-
PERPLEXITY_CONFIG_DIR Override config dir (default: ~/.perplexity-mcp)
|
|
1245
|
-
PERPLEXITY_VAULT_PASSPHRASE Env-var master-key fallback for headless Linux
|
|
1246
|
-
PERPLEXITY_MCP_STDIO=1 Forces stdio-server mode (no prompts)
|
|
1247
|
-
PERPLEXITY_NO_DAEMON=1 'daemon attach' runs in-process stdio (bypass daemon)
|
|
1248
|
-
`;
|
|
1249
|
-
|
|
1250
|
-
const DAEMON_HELP_TEXT = `perplexity-user-mcp daemon
|
|
1251
|
-
|
|
1252
|
-
Usage:
|
|
1253
|
-
npx perplexity-user-mcp daemon start [--port N] [--tunnel]
|
|
1254
|
-
npx perplexity-user-mcp daemon stop
|
|
1255
|
-
npx perplexity-user-mcp daemon status [--json]
|
|
1256
|
-
npx perplexity-user-mcp daemon attach [--fallback-stdio] [--ensure-timeout-ms N]
|
|
1257
|
-
npx perplexity-user-mcp daemon rotate-token
|
|
1258
|
-
npx perplexity-user-mcp daemon install-tunnel
|
|
1259
|
-
npx perplexity-user-mcp daemon enable-tunnel
|
|
1260
|
-
npx perplexity-user-mcp daemon disable-tunnel
|
|
1261
|
-
npx perplexity-user-mcp daemon list-providers [--json]
|
|
1262
|
-
npx perplexity-user-mcp daemon set-provider <cf-quick | ngrok | cf-named>
|
|
1263
|
-
npx perplexity-user-mcp daemon set-ngrok-authtoken <TOKEN>
|
|
1264
|
-
npx perplexity-user-mcp daemon set-ngrok-domain [<DOMAIN>]
|
|
1265
|
-
npx perplexity-user-mcp daemon clear-ngrok
|
|
1266
|
-
|
|
1267
|
-
Cloudflare named-tunnel setup (persistent URL on your own zone):
|
|
1268
|
-
npx perplexity-user-mcp daemon cf-named-login [--yes]
|
|
1269
|
-
Run 'cloudflared tunnel login' (opens browser, writes ~/.cloudflared/cert.pem).
|
|
1270
|
-
npx perplexity-user-mcp daemon cf-named-list [--json]
|
|
1271
|
-
List tunnels visible to the origin cert.
|
|
1272
|
-
npx perplexity-user-mcp daemon cf-named-create --name NAME --hostname HOST [--yes] [--json]
|
|
1273
|
-
Create a new tunnel + DNS CNAME, then write the managed config.
|
|
1274
|
-
npx perplexity-user-mcp daemon cf-named-bind --uuid UUID --hostname HOST [--yes] [--json]
|
|
1275
|
-
Bind the managed config to an existing tunnel UUID (credentials must exist
|
|
1276
|
-
at ~/.cloudflared/<uuid>.json). No browser, no DNS changes.
|
|
1277
|
-
|
|
1278
|
-
Notes:
|
|
1279
|
-
- --yes skips the y/N confirmation prompt for login / create / bind.
|
|
1280
|
-
- cf-named-login / create / bind prompt on stderr and read stdin; exit 130 on decline.
|
|
1281
|
-
- With --json, stdout is a single parseable JSON line (scriptable).
|
|
1282
|
-
|
|
1283
|
-
Environment:
|
|
1284
|
-
PERPLEXITY_NO_DAEMON=1 'daemon attach' runs in-process stdio (bypass daemon)
|
|
1285
|
-
`;
|
|
1286
|
-
|
|
1287
|
-
/* v8 ignore start -- only runs when cli.js is executed as a script */
|
|
1288
|
-
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1289
|
-
const parsed = parseArgs(process.argv.slice(2));
|
|
1290
|
-
routeCommand(parsed).then((res) => {
|
|
1291
|
-
if (res.stdout) process.stdout.write(res.stdout);
|
|
1292
|
-
if (res.stderr) process.stderr.write(res.stderr);
|
|
1293
|
-
process.exit(res.code);
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
/* v8 ignore stop */
|
|
1297
|
-
|
|
1298
|
-
export { parseArgs, routeCommand };
|
|
1
|
+
export function parseArgs(argv: any): {
|
|
2
|
+
command: string;
|
|
3
|
+
flags: {};
|
|
4
|
+
positional?: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
command: any;
|
|
7
|
+
flags: {};
|
|
8
|
+
positional: any[];
|
|
9
|
+
};
|
|
10
|
+
export function routeCommand(parsed: any): Promise<{
|
|
11
|
+
code: any;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}>;
|