All notable changes to this project are documented here. Format follows Keep a Changelog; versioning is SemVer.
-
Prompts tab — build custom MCP slash-commands from the dashboard (
PromptsTab.tsx,prompts-config.ts,prompts.ts,prompts-handler.ts). A new Prompts tab lets you create, edit, delete, and reset reusable prompt templates (name+description+arguments+ a{arg}-substitutedtemplate). They persist to~/.perplexity-mcp/prompts.json({ overrides, custom }, atomic write viasafeAtomicWriteFileSync) and are served through the standard MCP prompts capability — so they appear as/slash-commandsin every connected client (Claude Desktop, Cursor, …) with no per-IDE config writing. Ships five built-ins (perplexity.researchPlan,perplexity.reasoningPlan,perplexity-fact-check,perplexity-compare,perplexity-latest); built-ins can be overridden and reset to default, custom prompts can be deleted. Built-in defs + read/write/merge/render live once in the mcp-server (prompts-config, exported via the newperplexity-user-mcp/prompts-configsubpath) and are imported by the extension — single source of truth. The embedded daemon rebuilds its MCP server per request, so edits apply on the next connection automatically; long-lived stdio servers need a client reconnect (an in-UI notice explains how). Ported from the sibling Airtable extension's Prompts feature. -
Windsurf → Devin Desktop rebrand, with legacy support (
constants.ts,auto-config/index.ts). Cognition rebranded the Windsurf editor to Devin Desktop on 2026-06-02 (in-place OTA rename) and moved workspace rules from.windsurf/rules/to.devin/rules/, keeping.windsurf/rules/as a read fallback. The dashboard now labels the target "Devin Desktop (Windsurf)" (and "Devin Desktop Next (Windsurf Next)") and auto-config dual-writes rules to both.devin/rules/perplexity-mcp.md(new primary) and.windsurf/rules/perplexity-mcp.md(legacy — for pre-rebrand Windsurf and the vendor fallback); detection recognizes either, and removal cleans both. Thewindsurf/windsurfNexttarget keys are unchanged (existingautoConfigureWindsurf/mcpTransportByIdesettings keep working), and the MCP config path stays.codeium/windsurf/mcp_config.json(unchanged per the vendor docs). NewIdeMeta.legacyRulesPathsfield drives the dual-path behavior. Sources: Devin Desktop FAQ, Cascade Memories / rules docs.
- New unit tests:
test/prompts-config.test.js+test/prompts.test.js(mcp-server —{arg}substitution, merge/override flags, upsert/delete/reset, read-write round-trip with a tempPERPLEXITY_CONFIG_DIR, dynamic registration against a realMcpServer) andtests/prompts-handler.test.ts+tests/auto-config.devin-rules.test.ts(extension — prompts save/delete/reset validation; Devin/Windsurf dual-write + legacy detect + remove). Full build + typecheck clean across all four packages; 1225 tests pass. Still needs the manual VSIX smoke before a release is tagged — the live dashboard render and the rules-file//slash-command-in-a-client round-trips can't be exercised headlessly.
Follow-up UX for the 0.8.49 passphrase fix. After a profile switch or "Refresh state" the extension re-supplies the vault passphrase and hot-reloads the daemon — a headed reinit that takes ~30s. The daemon badge kept showing the stale "anonymous" message during that window, so it looked stuck even though it was working (confirmed in the field: the daemon goes green ~30s after a switch).
- Reconnecting spinner on the daemon badge (
views.tsx,store.ts,App.tsx). A transientreconnectingstore flag is set whenprofile:switch/dashboard:refreshis sent and cleared when the daemon next reportsauthenticated(or a 60s safety timeout). While active and the daemon is not yet authenticated, the badge shows a spinning "Reconnecting the daemon to this profile… this can take ~30s." instead of the stale anonymous/vault-locked status — so the wait reads as progress, not a hang.
Reported via a diagnostics bundle: after switching to a passphrase-protected profile, the dashboard showed "Session active and ready" while the daemon showed "Daemon sees anonymous session — use Refresh state to reconnect", and Refresh did not help. Root cause: the long-lived daemon is spawned with
PERPLEXITY_VAULT_PASSPHRASEread from SecretStorage once at spawn; a profile switch only touched.reinit, which re-raninit()with the stale/absent passphrase, so the daemon could not unseal the new profile's vault (Vault locked: no keychain, no env var, no TTY). The vault unseal cache also pinned the first profile's material (Vault decrypt failed: wrong passphrase). The extension could read the vault (the user had typed the passphrase) — hence the two badges disagreed.
- The daemon now re-receives the vault passphrase on profile-switch / "Refresh state" instead of staying anonymous. New loopback-only, bearer-authed
POST /daemon/reinit {passphrase}(daemon/server.ts) setsPERPLEXITY_VAULT_PASSPHRASEin the running daemon, clears the vault unseal cache, and hot-reloads the client — no daemon restart, no dropped MCP connections. The endpoint is blocked from tunnel callers by the H11 admin allowlist (404) plus an in-handler loopback check (defense-in-depth); the audit log records only method+path, never the body. client.reinit({ passphrase })(client.ts) sets the env andvault.__resetKeyCache()only when a passphrase is supplied, so a passphrase change isn't blocked by the previous profile's pinned_unsealMaterialCache/_keyCache. The reset is deliberately scoped: the OS-keychain master key is shared across all profiles (KEYTAR_ACCOUNT = "vault-master-key"), so keychain-based unseals (typical macOS/Windows) stay valid across a profile switch and are never reset — clearing the cache would re-import keytar and re-trigger macOS Keychain permission prompts (issue #6 bug 3). Linux/passphrase users are unaffected (they supply the passphrase and get the reset they need).- The extension re-supplies the passphrase (
DashboardProvider.ts): "Refresh state", webviewprofile:switch, and the command-palettePerplexity.switchAccount/Perplexity.addAccountnow POST the SecretStorage passphrase to/daemon/reinit(non-spawning — it only targets an already-running daemon; falls back to touching.reinitif the HTTP path is unavailable or returns non-2xx).
DaemonAuthStatus.reason(ok/vault-locked/not-logged-in) in shared, surfaced fromgetSavedCookies()→wasLastVaultLocked()→init()→writeDaemonStatus(). The dashboard's daemon badge is now reason-aware (views.tsx) — it distinguishes "daemon can't unlock this profile's vault" from "not logged in" instead of always saying "use Refresh state".
- An animated "reconnecting…" loader while a daemon reinit is in flight is deferred (needs transient webview-store state). One vault passphrase per machine is intentional (global SecretStorage key); per-profile passphrases are out of scope.
- New unit tests:
test/daemon/reinit-http.test.js(bearer required, passphrase forwarded, empty/missing handled),/daemon/reinitadded to the tunnel-admin-allowlist suite (tunnel→404, loopback→handler), andwasLastVaultLocked()coverage intest/config-getSavedCookies.test.js. Adversarial review (4 dimensions) — all confirmed findings fixed (command-palette wiring,res.okfallback, no-spawn). Full typecheck + build clean across all four packages. Live daemon + passphrase-profile-switch flow on Windows still needs the manual smoke (the SecretStorage→daemon round-trip can't be exercised without a real daemon).
Ref #8. Regression from the 0.8.43 issue-#5 fix (commit
c841124): Phase 2 (headless search) switched from an ephemeralchromium.launch()to a long-livedlaunchPersistentContext(browser-data)so it could reuse the on-diskcf_clearancewritten by Phase 1. The side effect — the daemon now holds thebrowser-dataChromium process-singleton lock for its entire lifetime, and Chromium forbids a secondlaunchPersistentContexton the same--user-data-dir. Three independent callers still targeted that dir: the Doctor probe (a separate process that ran its owninit()againstbrowser-data), the daemon's own Phase 1→Phase 2 transition, and the reinit cycle. On Windows each collision surfaced asexitCode 21/ "Target page, context or browser has been closed", leaving the daemon stuck in anonymous mode in a self-reinforcing deadlock that only a full restart cleared. The fix makes the daemon the single owner ofbrowser-dataand hardens the lock handling — without reverting thecf_clearancereuse.
- The Doctor probe no longer launches a competing browser against
browser-datawhen a daemon is live (checks/probe.js). NewliveDaemonOwnsProfile()reads the daemon lockfile (daemon/lockfile.jsread+isStale+isProcessAlive); when a live daemon owns the profile, the probe returns askip(the daemon's real auth state is already surfaced by theprofilescheck'sdaemon-status) instead of opening a second persistent context that would collide on the profile singleton lock. This was the deterministic Doctor-failure in the report, and it also removes one of the racers that could knock the daemon out of its own reinit. - Both daemon
browser-datalaunches now clear stale locks and retry with backoff (client.tsPhase 1headedBootstrapand Phase 2init). On Windowscontext.close()resolving does not release the profile lock synchronously, so the Phase 1→Phase 2 transition (and a reinit immediately after login) could momentarily collide on the same--user-data-dir. The newlaunchWithRetry()rides out that release lag instead of failing the wholeinit()into anonymous mode. clearStaleSingletonLocksnow also clears the Windowslockfile(fs-utils.js). It previously removed only the POSIXSingletonLock/SingletonCookie/SingletonSocketfiles, so on Windows — where the ProcessSingleton lock islockfilein the user-data-dir root — a stale lock was never cleared andlaunchPersistentContextkept failing. (Default/LOCKis a LevelDB lock, not the process singleton, and is deliberately left alone.)
launchWithRetry()andisLockContentionError()infs-utils.js(+fs-utils.d.ts). A generic, fully-injectable (sleep/beforeAttempt/isRetriable) bounded-retry helper that retries only transient Chromium profile-lock collisions and rethrows everything else immediately.isLockContentionErrormatches the WindowsexitCode 21/ "Target page, context or browser has been closed" / process-singleton signatures.liveDaemonOwnsProfile()exported fromchecks/probe.js— single source of truth for "does a live daemon own this profile's browser session", with adaemonOwnsOverridetest seam.
- New unit tests:
test/fs-utils.test.js(+7: Windowslockfileclearing,launchWithRetrybackoff/retry/rethrow/no-retry,isLockContentionErrorsignatures) andtest/probe-daemon-owner.test.js(live/absent/stale daemon detection; probe skips without launching when a daemon is live).test/checks/probe.test.jsmade hermetic against a real running daemon viadaemonOwnsOverride. Full mcp-server unit suite (595 tests) passes; typecheck clean across all four packages. Live daemon/login flow on Windows still needs the manual VSIX smoke — the lock-release timing can only be exercised against a real Chrome + daemon.
Ref #9. On macOS the bundled Chromium flashed a visible window for ~1–2s during login / re-login / "Refresh state" and during the daemon's background session refresh, before minimizing or closing.
--start-minimizedwas already passed but Chromium paints the window before honoring it, and the daemon's CF-solving bootstrap had no positioning at all. Both headed paths now paint the window off-screen from the first frame, matching the already-invisible headless search experience. The interactive manual-login window (which the user must see to solve a real Cloudflare challenge or enter an OTP in-page) is intentionally left visible.
- The headed auto-OTP login window no longer flashes visible (
login-runner.js). It launches with--window-position=-32000,-32000so the window is created off-screen instead of relying on the post-paint CDP minimize that--start-minimized+minimizePageWindow()performed too late on macOS. This window never needs user interaction (the OTP arrives over IPC from the extension UI and Cloudflare auto-solves on a clean IP; a real CF challenge bails to the manual runner), so off-screen is always safe. - The daemon's headed CF-solving bootstrap no longer flashes visible (
client.tsheadedBootstrap/buildLaunchOptions). The headed Phase-1 launch now appends the same off-screen position arg. Previously it had no positioning and stayed fully visible for the entire bootstrap, which is the "flash while the daemon refreshes the session" the issue describes. Headless launches (search) are unchanged — they have no window.
src/browser-window.{js,d.ts}now exportsOFFSCREEN_POSITION_ARG(--window-position=-32000,-32000) andloginLaunchArgs(localOrigin), the single source of truth for the off-screen flag shared by both headed launch sites. The window is moved off-screen rather than shrunk (--window-size=1,1) or run truly headless, because an abnormal viewport or headless fingerprint is a Cloudflare/Turnstile bot tell — the real headed fingerprint is exactly what keeps the cookie-grab passing CF.
- New unit tests:
test/browser-window.test.js(4) andtest/client-launch-options.test.js(2) assert the headed paths include the off-screen arg, the headless paths do not,--start-minimizedis retained as a Windows backstop, and--window-size=1,1is never added. Full mcp-server unit suite (583 tests) and thelogin-runnerintegration test (5) pass on Node 22 / Windows; typecheck clean across all four packages.
Refs #5 and #6. Two stacked regressions caused the daemon to report anonymous mode after a successful login:
PERPLEXITY_HEADLESS_ONLY=1leaked from the launcher env into the daemon's spawn env (disabling the headed bootstrap entirely), and Phase 2 used a non-persistent browser context that discarded the freshcf_clearanceacquired in Phase 1. Separately, a symlink-detection bug silently no-oped the CLI on POSIX Homebrew/npm global installs, and repeated keychain probes triggered macOS Keychain permission prompts on every diagnostic pass.
PERPLEXITY_HEADLESS_ONLYandPERPLEXITY_NO_DAEMONare now stripped from the daemon spawn env in all three spawn sites (daemon/launcher.ts,extension/src/daemon/runtime.ts,extension/src/auto-config/transports/stdio-daemon-proxy.ts). These are launcher-scoped flags that must never reach the daemon's ownPerplexityClient.init()— leaking them forced the daemon to skip the headed bootstrap (the only phase that can solve CF challenges) or bypass daemon attach entirely. Fixes the "anonymous mode despite successful login" regression.- Phase 2 (headless search) now uses a persistent browser context (
chromium.launchPersistentContext(browserData, ...)) sharing the same profile directory as Phase 1. The freshcf_clearancewritten to disk by the headed bootstrap is loaded automatically, eliminating the CF-challenge failure that caused every subsequent search to see anonymous mode. Vault cookies are injected selectively — only cookies not already present on disk are added, so a freshly-writtencf_clearancefrom Phase 1 is never overwritten by a stale vault copy. - CLI and daemon entrypoints now resolve symlinks in the direct-run guard (
isMainModule()in newsrc/is-main-module.js). The previousimport.meta.url === pathToFileURL(process.argv[1]).hrefcheck silently returnedfalsewhen the binary was invoked through a symlink (npm global install, Homebrew Cellar,node_modules/.bin/), causing the CLI and daemon to exit0with no output. Bothcli.jsandindex.tsnow userealpathSyncon both sides with a defensive fallback. dist/cli.mjsnow has a shebang and executable mode after build. A newscripts/post-build-shebang.mjspost-build script prepends#!/usr/bin/env nodeandchmod 755s the file. tsup intentionally omits the shebang (it trips vitest/esbuild during test imports); the post-build script targets onlydist/cli.mjs. No-op on Windows where npm uses.cmdwrappers.- Keytar probe results are cached per-process in
vault.js. PreviouslytryKeytar()re-imported the native module on every vault read, triggering macOS Keychain permission dialogs repeatedly within a single session. The new_keytarModuleCachevariable caches success and failure alike;__resetKeyCache()clears it on profile-state changes.probeKeychainState()is exported for shared use acrosscli.jsandchecks/vault.js, removing three duplicate inline probe implementations. PERPLEXITY_DISABLE_KEYCHAIN=1environment variable disables all keychain access. Useful in headless test environments and CI where no credstore is available and the "no keychain" code path should be taken without a failed import attempt.- Login runners now use a persistent browser context (
login-browser-data/under the active profile directory, separate from the daemon'sbrowser-data/). Google SSO and Cloudflare state accumulated across login sessions — eliminating the "log in constantly" UX paper cut. The separate directory avoids Chromium singleton-lock collisions with the daemon's headed bootstrap. getSavedCookiesnow emits specific diagnostic log lines for each empty-return path: novault.enc(run login first), vault exists butcookieskey absent, cookies value is not an array, and JSON parse failure. AnunsealFailedflag prevents the "key absent" message from firing when the real cause is an unseal error (which is already logged by the catch path).
src/is-main-module.js+src/is-main-module.d.ts— shared symlink-aware direct-run guard extracted fromcli.jsandindex.ts.scripts/post-build-shebang.mjs— post-build shebang injection + chmod fordist/cli.mjs. Runs as part ofnpm run buildinpackages/mcp-server.probeKeychainState()exported fromvault.js— single keychain probe entry point with caching andPERPLEXITY_DISABLE_KEYCHAINawareness.
- Build script (
packages/mcp-server/package.json):tsup→tsup --no-dts && tsc --allowJs --emitDeclarationOnly && node scripts/post-build-shebang.mjs. tsup's rollup-dts worker OOMs on this package's 47+ entry points under Node 22+/Windows;tsc --allowJs --emitDeclarationOnlyis faster and avoids the worker heap ceiling. tsup.config.ts:dts: true→dts: false; comment updated to reflect the build script split.
- All 130 test files pass (1151 tests, 2 intentionally skipped) on Node 22 / Windows.
- Full typecheck clean across all four packages.
Refs #3. Driver: an external user (Claude Code on Win11) hit "Vault locked" because the extension-managed daemon never received the SecretStorage passphrase, AND the launcher silently fell back to direct vault access in the client's runtime.
- Daemon spawn now receives the SecretStorage passphrase via a narrowly-scoped env builder. The
configureDaemonRuntimeconfig gained an optionalbuildDaemonEnvasync provider; the extension wires() => buildDaemonEnv(context)which callspeekStoredVaultPassphrase. Provider env is merged AFTERprocess.envand BEFORE the hard-codedELECTRON_RUN_AS_NODE/PERPLEXITY_CONFIG_DIR/PERPLEXITY_OAUTH_CONSENT_TTL_HOURSoverrides — the provider cannot clobber critical spawn env. Passphrase status is logged asset/unsetonly; the value never appears in logs and the extension host's ambientprocess.envis never mutated. - Generated
stdio-daemon-proxylauncher refuses silent fallback to in-process stdio. Pre-0.8.41, when daemon attach failed, the launcher would spawn a fresh in-process MCP server in the client's Node runtime — which on Claude Code (Node 24+), Antigravity, or any non-Electron runtime would then try to readvault.encwith no SecretStorage access and no keytar that loads. Now the launcher catches a typedDaemonAttachError, writes a structured remediation to stderr only (stdout is the JSON-RPC framing channel), and exits 2 (operator-actionable misconfiguration).
DaemonAttachErrorinpackages/mcp-server/src/daemon/attach.tswithcode: "DAEMON_UNREACHABLE",remediation: readonly string[], optionalcause. Used by the launcher and bycli.js'sdaemon:attachsubcommand.attach.tsis forbidden from callingprocess.exit— the entrypoint layer (launcher, CLI) owns process-termination semantics.- Reserved exit code
2for "operator-actionable misconfiguration" (distinct from1= generic crash). Documented in launcher comments. docs/vault-unseal.md— was referenced from the "Vault locked" error message since v0.4.x but never existed. Now documents the keychain → env var → TTY unseal chain, standalone vs. extension-managed paths, per-platform notes, and recovery flow.docs/troubleshooting/external-mcp-clients.md— single canonical page for users hitting "Vault locked" or "DAEMON_UNREACHABLE" from external IDEs (Claude Code, Antigravity, Codex CLI, Cursor). Linked from both READMEs.- Softened the Windows-keychain "just works" claim in
docs/codex-cli-setup.mdwith a "what if it fails" paragraph pointing atsetup-vaultand the new recovery doc. - Repo tooling: pre-push hook (
scripts/git-hooks/pre-push) refuses to publishdocs/superpowers/paths. Auto-installed vianpm installpostinstall.
- CI matrix: Node 20 → Node 22 + Node 24. Node 20 reached End-of-Life on 2026-04-30. Resolved two pre-existing Node-20-specific failures (Linux tsup DTS worker OOM + Windows leaked FSWatcher in
launcher.test.js). engines.node:>=20→^22.0.0 || ^24.0.0in bothpackages/extension/package.jsonandpackages/mcp-server/package.json. Matches what we test; pattern lifted from Vite/Vitest's engines style.
- No breakage for users on Win11/macOS with working keychain. Daemon runs; attach succeeds; business as usual.
- Behavior change for users currently relying on the silent in-process fallback: they now see an actionable stderr remediation instead of "anonymous mode" silently. This is the intended outcome — issue #3 reporters are exactly this cohort.
- The 0.8.40 launcher on disk gets rewritten by
ensureLauncher's byte-comparison logic on next extension activation. No manual user action needed.
- Phase 0 keytar probe passed on Win11 + VS Code Code.exe (Electron 39.6.0, Node 22.22.0 internally) — keytar loads reliably under the daemon's spawn runtime.
- All 4 CI matrix entries green: ubuntu-latest × {22, 24}, windows-latest × {22, 24}.
- Manual smoke (Win11 + Claude Code Node 24+ →
perplexity_reasonreturns Pro reply) gates issue #3 closure; recorded indocs/smoke-tests.mdpost-release.
- Envelope v4 vault format (multi-source unseal envelopes) — Phase 0 verification passed on the daemon's actual spawn runtime, so v4 is no longer load-bearing for closing #3. Tracked as future hardening.
- HTTP loopback port-drift UX — scheduled for 0.8.43.
keytar → @napi-rs/keyringswap — 0.9.x hardening track.
Versioning note: 0.8.29 through 0.8.39 were local pre-release iterations and never tagged. The cumulative work below — IDE expansion, login deadlock fixes, profile-switch propagation, vault key-rotation tolerance, CLI vault setup wizard — is rolled into this release. Diagnostics from a real user session (
perplexity-mcp-diagnostics-2026-05-04T*) drove the auth + vault fixes.
- 7 new file-based MCP clients, each verified against primary-source docs and unit-tested for path resolution + root-key shape:
- Visual Studio 2022 (workspace
<sln>/.mcp.jsonor~/.mcp.json, root keyservers, stdio withtypediscriminator) — auto-configurable. - OpenCode (
~/.config/opencode/opencode.json, root keymcp, OpenCode's local-server entry shape withtype:"local"+command:[node,server]+enabled:true+environmentblock) — auto-configurable. - GitHub Copilot CLI (
~/.copilot/mcp-config.json, root keymcpServers) — auto-configurable. - Factory Droid (
~/.factory/mcp.json, root keymcpServers) — auto-configurable. - Qwen Code (
~/.qwen/settings.json, root keymcpServers) — auto-configurable. - Kilo Code (JSONC at
~/.config/kilo/kilo.jsonc, root keymcp) — registry-only; auto-config gated until JSONC writer lands so user comments aren't stripped. - LM Studio (
ui-only;mcp.jsonis GUI-managed at an unstable path) — registry-only.
- Visual Studio 2022 (workspace
jsonConfigRootKey: "mcp"added toIdeMeta's union, alongsidemcpServers/servers/context_servers. OpenCode and Kilo Code use it.jsonServerEntryFormat: "standard" | "opencode"capability lets a target opt into a non-standard server-entry shape; OpenCode's local-server format is the first user. Future shape capabilities can be added without touching the auto-config writer.
npx perplexity-user-mcp setup-vault [--profile X] [--json] [--probe-only]— a read-only diagnostic + setup wizard for the standalone CLI, mirroring the extension dashboard's existingensureVaultPassphrasepopup. Probes keychain availability + master-key persistence, env-var passphrase, and (when a profile is given) whether the on-diskvault.encactually decrypts with the resolved unseal material. Recommends the right action: nothing-to-do when a path is configured,logout --purgewhen an existing blob can't be decrypted, "setup needed" when no path exists. For setup-needed cases, generates a 256-bit base64url passphrase and prints cross-platform persistence snippets matchingprocess.platform:- Windows: PowerShell
[Environment]::SetEnvironmentVariable(..., "User")(persistent user-scope) +setxfor cmd.exe. - macOS: zsh
~/.zshrc(default since Catalina) + bash~/.bash_profile(legacy). - Linux: bash
~/.bashrc+ zsh~/.zshrc+systemd Environment=line for service deployments. - All platforms: the cross-platform MCP-client env-block snippet (Cursor / Claude Desktop / Codex CLI all share the same JSON env shape) listed first as the recommended choice — scoped to one client only, no shell leakage.
- Windows: PowerShell
spawnRunnerfor login now acceptsAbortSignalandtimeoutMs. Previously the browser-fallback path (packages/extension/src/mcp/auth-manager.ts) calledspawnRunnerwith no timeout. When impit failed withcf_blockedand the browser runner spawned but didn't complete (Cloudflare turnstile didn't surface, user closed the window without auth, runner crashed without writing JSON), the spawn promise hung forever, the inflight-lock map kept the lock, and every retry bounced withLogin already in progress for X— the user had to reload the extension to recover.spawnRunnernow propagates abort via SIGTERM (escalating to SIGKILL after 5s grace) so a runner that ignores SIGTERM still releases the lock.AuthManager.login()registers anAbortControllerper profile and threads it throughrunLogin→runOneRunner→spawnRunner. NewLOGIN_TIMEOUT_MS = 5*60_000(env-overridable viaPERPLEXITY_LOGIN_TIMEOUT_MS) caps the wall-clock — comfortably covers Cloudflare turnstile + OTP + SSO; longer than that is stuck.AuthManager.cancelLogin(profile)breaks out of an in-flight login. Surfaced as a webviewauth:cancelmessage +DashboardProviderhandler + a Cancel button in the dashboard that replaces the Login button while status islogging-inorawaiting_otp. Clicking it aborts the runner and clears the lock immediately so the next click doesn't bounce.
reinit-watcher.jsgainswatchActiveProfile(configDir, cb)that watches<configDir>/activeforsetActive()'s atomic rename. The per-profilewatchReinitis bound to a single profile's.reinitfile captured at daemon startup; if the user switched the active profile, that watcher would never see anything (the new profile's.reinitis in a different directory) and the daemon's in-memory browser context kept serving stale cookies — manifested as "UI says I'm Pro, but MCP responds as Free" / "switching profiles doesn't take effect until I restart my IDE".- Both packages/mcp-server/src/daemon/launcher.ts and packages/mcp-server/src/index.ts (stdio entrypoint) now register
watchActiveProfilealongsidewatchReinit. On active-pointer change, the daemon disposes the old per-profile watcher, rebinds to the new active profile, and callsclient.reinit()so cookies for the new profile load immediately. Both watchers are disposed on shutdown.
readVaultObjectnow iterates EVERY available unseal material in preference order (keychain key → env-var passphrase) instead of just the preferred one. AES-GCM auth failures continue the chain; structural errors (corrupt JSON, scrypt floor violation) bail immediately. The first material that successfully decrypts is pinned via_unsealMaterialCacheso subsequent reads in the same process don't re-pay the failed derivations.Vault.setcatches read failures whose message matches theVault decrypt failed/wrong passphrase or corrupted ciphertextsurface, quarantines the dead blob tovault.enc.unreadable.<ts>.bakfor forensic recovery, and proceeds with an empty object so the write produces a fresh blob encrypted under whichever material is currently winning. A login that's about to overwrite the cookies key anyway shouldn't be blocked by an undecryptable old blob.- Reproduces from a real user session: profile
vault.encwas written underPERPLEXITY_VAULT_PASSPHRASEonly (keytar wasn't loading on 0.8.38), then the 0.8.39 upgrade got keytar working andgetUnsealMaterialflipped its preference to the keychain key — which couldn't decrypt a passphrase-encrypted blob. Login crashed withVault decrypt failedduringVault.set's read-merge-write step, locking the user out of even fresh logins. - New exported helper
getAllUnsealMaterials()returns every available unseal context in preference order, mirroringgetUnsealMaterialwhich still returns only the preferred one for the write path.
- CLI preflight (packages/mcp-server/src/cli.js
checkVaultUnseal()) runs beforeadd-accounttouches the profile dir orloginspawns the runner. If no unseal path is available (no keychain binding, no env var, no TTY), exits 1 with a platform-aware setup hint pointing atsetup-vault, before any browser opens. Bypassable via--skip-vault-checkfor setups where the daemon owns the vault. - Doctor
unseal-verifycheck (packages/mcp-server/src/checks/vault.js) — whenvault.encexists AND a current unseal material is available, the doctor now actually attemptsVault.geton it. If decrypt fails (the cross-keying scenario), emits aunseal-verify: failwith a recovery hint pointing atlogout --purgeand explaining that v0.8.40+ self-heals on next login. Catches the "I have valid credentials but the old vault is undecryptable" state that a simpleunseal-path: passline would mask.
- Antigravity flipped to
autoConfigurable: falseuntil~/.gemini/antigravity/mcp_config.jsonis documented in first-party Antigravity docs. Was previously enabled based on third-party guides only; risked silently writing to a path the product doesn't actually read. - Warp converted to
configFormat: "ui-only"withstdio: false. Warp's MCP servers are configured exclusively through the in-app Settings UI; there's no documented file-based config. The~/.warp/mcp.jsonpath is now a detection sentinel only with an explanatory comment. - Rules-tab copy no longer claims VS Code writes to
.github/instructions/—vscodehasrulesFormat: "none"; onlycopilotwrites there. - Removed
antigravityfromAUTO_CONFIGURABLE_IDESso "Configure for All" doesn't write to the unverified path.
packages/mcp-server/README.md now leads with a 6-row pivot table (desktop + extension / desktop + CLI manual / desktop + CLI auto / headless VPS w/ email / headless VPS w/o browser / headless VPS + desktop daemon) so users land on the right path immediately instead of trying manual mode on a headless box and hitting cryptic browser-launch errors. Adds a Multiple accounts / profiles section walking through add-account / list-accounts / switch-account. Adds a Headless / VPS deployment section with three patterns (terminal-only login via impit+OTP, pre-supplied PERPLEXITY_SESSION_TOKEN, daemon+tunnel from a desktop). Documents that npx perplexity-user-mcp with no subcommand starts the stdio MCP server and waits silently — that's expected, not a hang.
- +19 new tests, full suite 1113 passing.
auto-config.test.ts— 8 new path-resolution + write-shape tests for VS 2022 / Copilot CLI / OpenCode / Factory / Qwen Code, plus the OpenCodemcproot-key + local-server-shape write contract.auth-manager.login.test.ts— 3 new tests: timeout fires + clears the inflight lock,cancelLoginbreaks a hung runner immediately,cancelLoginreturns false when no login is in flight.reinit-watcher.test.js— 2 new tests forwatchActiveProfile: fires onsetActive, dispose stops firing.vault.test.js— 2 new tests: read falls back across unseal materials when keychain key changes after a passphrase-only write,Vault.setquarantines an undecryptable blob and writes a fresh one.cli.test.js— 8 new tests: 3 vault-unseal preflight (fails fast, proceeds when env var set,--skip-vault-checkbypasses) + 5 setup-vault (keychain-with-key, env-var-only, no-path generates passphrase + cross-platform snippets,--probe-onlysuppresses generation, broken-decrypt branch).
vault.jscoverage stays at 98.34% lines / 100% functions, well above the 95% floor.
Versioning note: 0.8.20 through 0.8.27 were local pre-release iterations and were never tagged. The cumulative work (cloud-sync timeout fixes, retrieve-via-impit, search-pilot, plus the four phases below) is rolled into this release. Plan: docs/impit-coverage-plan.md.
- Phase 1 — Impit-driven
perplexity_login(auto mode), opt-in. New runner at packages/mcp-server/src/impit-login-runner.js drives the existing 6-step Perplexity email+OTP flow (csrf → sso check → signin/email → wait OTP → otp-redirect → callback) through impit instead of through a Patchright browser. The big visible Chrome window goes away on auto-login; OTP entry happens in the dashboard webview (existing UI) or CLI (readline-style prompt). Falls back to the existing browser runner on impit-only failures (cf_blocked,impit_missing,impit_load_failed,auto_unsupported,crash); user-facing failures (otp_rejected,sso_required,email_rejected) are surfaced directly. Gated behindPERPLEXITY_EXPERIMENTAL_IMPIT_LOGIN=1(env var) for the public release. - CF warmup helper at packages/mcp-server/src/cf-warmup.ts. Brief headless Chromium launch (~1-2s, capped at ~12s) that captures
cf_clearancefor the impit pipeline. Skipped when the vault already has it. This is the only browser surface remaining in the impit-login pilot — Phase 7 (post-public) explores raw-impit Turnstile solving to drop it entirely. CookieJarhelper at packages/mcp-server/src/cookie-jar.js (+ tests, 21 cases, all passing). RFC 6265-style Set-Cookie/Cookie round-trip used by the impit-login runner to capture session cookies across the OAuth callback redirect chain. Hand-rolled (no new npm deps) to honor the externalize-vs-bundle rules.- Phase 2 — CLI parity for impit-login.
npx perplexity-user-mcp login --mode auto --email <addr>honors the samePERPLEXITY_EXPERIMENTAL_IMPIT_LOGIN=1opt-in (or--impitflag) and falls back to the browser runner with the same reason set as the extension.--no-impitforces the browser path. - Phase 3 —
perplexity_exportimpit fast path. PDF / DOCX exports now go via two impit calls (entry-uuid resolve +POST /rest/entry/export) instead of throughpage.evaluate. Stable JSON contract — default-on, no env-var gate. Markdown remains a 100% local operation. Implemented asexportThreadViaImpitin packages/mcp-server/src/client.ts. - Phase 4 —
perplexity_modelsfrom-cache. Reads<configDir>/profiles/<name>/models-cache.jsondirectly via the newreadCachedAccountInfoFromDisk()helper, bypassing browser init when the cache is populated by an earlierrefresh.tstier-fetch. Falls back to lazygetClient()on missing/empty cache. Anonymous accounts still go through init (their cache has nomodelsConfig).
parseSSEText,parseASIReconnectSSE,extractFromWorkflowBlock,parseASIThreadEntrywere promoted fromprivateinstance methods tostaticso the standalone impit helpers can share the same response-parsing source of truth as the in-class browser path.auth-manager.tsrunLoginrefactored intorunLogin+runOneRunnerto support the impit→browser fallback. Behavior identical whenPERPLEXITY_EXPERIMENTAL_IMPIT_LOGINis unset.loadImpitandImpitModuleare now exports ofrefresh.tsso the impit-login runner can construct an Impit client directly.
cookie-jar.test.js— 21 cases covering Set-Cookie parsing, Domain/Path matching, Expires/Max-Age, Secure/HttpOnly, replace-on-same-triple, and round-trip throughtoPlaywrightShape().getClient-retry.test.jsupdated to setPERPLEXITY_CONFIG_DIRso the new cache-fast-path doesn't bypass the init() the test exercises.stealth-args.test.tsupdated to acceptstatic extractFromWorkflowBlock(wasprivate).
Versioning note: 0.8.18 was a local pre-release iteration and was never tagged.
- Cloud-sync via impit was returning 0 rows on every call while the same account had hundreds of threads on the web. Root cause:
listCloudThreadsViaImpitPOSTed without the Perplexity-specific request headers (x-app-apiclient,x-app-apiversion,x-perplexity-request-endpoint,x-perplexity-request-reason,x-perplexity-request-try-number) that Perplexity's frontend JS auto-injects on every same-origin fetch. The backend treats requests missing these as "no app context" and silently returns HTTP 200 with[]rather than 401 — so the sync looked successful but never imported anything. The browser path (pageFetchJson) was unaffected because Perplexity's own JS adds the headers when fetch fires from inside the page context. Discovered in 0.8.17 testing where every sync loggedlist_ask_threads via impit: 0 rows (offset=0 limit=100 total=0). - History-list cap was inconsistent across actions in the dashboard.
postHistoryListin DashboardProvider.ts was called with100after rebuild / search / hydrate / profile-switch and200after cloud-sync; the default was50. On stores larger than 100 entries this made the visible total flip between actions — the source of the "stats change when I click rebuild / when I click from Claude" reports. All call sites now use a single 200-cap default.
Versioning note: 0.8.13–0.8.16 were local pre-release iterations and were never tagged or published. The cumulative work (public-hardening followups: stealth-flag pruning, vault v3 KDF stretching, auto-config full tool list, CI heap + Windows browser-test fixes, webview-on-reload error catch) was rolled into this release alongside the cloud-sync work below.
- Browser-free cloud-sync fast path via impit. New
listCloudThreadsViaImpit+impitFetchJsonhelpers in client.ts / refresh.ts skip the headless Patchright launch entirely when impit (Speed Boost) is installed and a session cookie is on disk. The daemon'sperplexity_sync_cloudtool now passesgetClient(lazy) instead of an already-init'd client so the browser is only spawned if impit misses on a page; the first miss in a run sticks. Per-page success logs as[perplexity-mcp] list_ask_threads via impit: N rows ...to make engagement easy to verify. with_temporary_threads: trueinlist_ask_threadsPOST body, matching the captured browser-side request.
- Cloud-sync default page size 20 → 100 (cloud-sync.js, client.ts) — 5× fewer round trips per sync.
MAX_PAGESlowered from 200 to 50 so the runaway cap is still ~5000 threads. CloudSyncOptionsgains an optionalgetClient(lazy getter) used by the daemon to defer init until impit-fallback is needed; theclient(eager) form is preserved for the CLI and other callers that already paid for init.
- Stealth flags pruned (client.ts, refresh.ts) —
--disable-web-security,--disable-features=IsolateOrigins,site-per-process, and--disable-site-isolation-trialsremoved; the rationale is documented inline. Same-origin in-pagefetch()doesn't need them; the off-origin ASI download path moved toAPIRequestContext(which inherits cookies but isn't subject to CORS) so the security cost was unjustified. - Vault v3 KDF stretching with scrypt for passphrase-derived vaults; v1/v2 vaults migrate transparently on first unlock.
- Auto-config rules block now lists all 14 tools in the PERPLEXITY-MCP managed section so IDEs that read the rules file get an accurate inventory.
- Extension webview
already registerederror on host reload caught and logged instead of crashing activation.
- Tailwind oxide native binding force-installed on CI to work around npm/cli#4828.
NODE_OPTIONSheap raised to 4GB for the tsup DTS worker.- Browser-backed integration tests skipped on CI; per-OS test paths fixed.
- VSIX clean-check ignores vendored
.tsundernode_modules; tighter VSIX grep + platform-aware path validator.
- HTTP-loopback transport for Codex CLI with TOML bearer env headers in auto-config.
- Vault v2 (salted format). Passphrase-based vaults now use per-vault random salts for PBKDF2 key derivation; existing v1 vaults are migrated transparently on first unlock.
- Browser Runtime card now populates on initial dashboard load.
DashboardProvider.refresh()was missing thepostAuthState()call, so the BrowserSettings card stayed empty until an auth-state change event fired. - History tab cards resize with the sidebar instead of clipping. Added
min-width: 0to grid items,overflow-wrap: anywhereto text content, and constrained markdown code blocks to scroll within cards. - Pro tier inferred from ASI computer access when Perplexity omits explicit tier data in login metadata.
- Doctor now flags
code-insiderscommand paths and warns on non-node stdio command paths in IDE config audits. - Resolved node path passed to stdio config writers and stale config regen to prevent path-mismatch issues.
- Express alignment with SDK. Daemon express setup aligned with
@modelcontextprotocol/sdkinternal expectations. - ASI workflow blocks typed via discriminated union (refactor, no behavior change).
packages/mcp-server+packages/extensionbump to0.8.12.- Extension license aligned to MIT; publisher set to
Automations-Project; "Internal" removed from display name and descriptions. - NOTICE expanded to cover all significant runtime dependencies.
- README updated for public contributing workflow (PRs welcome, branch from main).
audit-log-path,oauth-rate-limit,security-helpersdaemon tests.login-tier-end-to-endintegration test for tier-inference fix.vault.test.jscovers v1→v2 migration and salted-format round-trip.validate-command,detect-ide-status-command,configure-targets-node-pathextension tests.capabilities.test.tsin shared package.
[0.8.10] — 2026-04-26 — Hygiene cycle: Obscura revert + safe-write + page.evaluate + Windows CI + Express 5 alignment
Versioning note: 0.8.6–0.8.9 were local pre-release iterations and were never tagged or published. The cumulative work was rolled into 0.8.10.
Broadens browser support from "Chrome or bundled Chromium, Windows/Linux-centric" to five runtimes with a usable UI contract across all three platforms; reverts the briefly-attempted Obscura CDP integration; and ships several Windows-friendliness fixes (atomic write helper, Singleton-lock cleanup, stale-version daemon reaper, Express 5 alignment) plus the first round of Windows CI.
- Brave Browser detection on Windows, macOS, and Linux. Treated as
channel: 'chromium'with an explicitexecutablePath, which is how Patchright handles Chromium forks natively. Wired into both mcp-server/config.ts::findBrowser and the new extension/browser/browser-detect.ts. - Microsoft Edge on macOS. Previously only probed on Windows and Linux; the macOS
/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edgepath is now in the candidate list. BrowserDownloadManagerat packages/extension/src/browser/browser-download.ts. Drivespatchright-core install chromiumwithPLAYWRIGHT_BROWSERS_PATHpointed at VS Code's per-extension globalStorage, parses progress from stderr, firesonDidChangeevents the dashboard can consume, supports clean removal, and SIGKILL-escalates on timeout so Windows never leaks a stuck download process.- Two new
PERPLEXITY_*env vars read by mcp-server/config.ts:PERPLEXITY_BROWSER_PATH— absolute path to any browser executable (supersedes legacyPERPLEXITY_CHROME_PATH, which still works).PERPLEXITY_BROWSER_CHANNEL—chrome/msedge/chromium.
AuthManagerbrowser orchestration at packages/extension/src/mcp/auth-manager.ts:attachDownloadManager,refreshBrowserDetection,setBrowserChoice, andsyncProcessEnv(writes the env vars ontoprocess.envso the detached daemon spawned in daemon/runtime.ts inherits the active selection). Runners spawned viaspawnRunnerreceive the same env vars.- Shared wire types in packages/shared/src/messages.ts:
BrowserInfo,BrowserChoice,BrowserDownloadState,BrowserChannel.AuthStatenow carriesbrowser,availableBrowsers,browserDownload,browserChoiceso a future dashboard browser-picker UI has everything it needs over postMessage.
- "Capture diagnostics" button moved from the Singleton Daemon panel to the Doctor tab header. The action lives next to Run / Deep check / Export — the bundle includes a doctor report, so the Doctor tab is its natural home. The button now renders a spinner + "Capturing…" label while the file-save dialog and zip write are in flight (was previously silent), and is disabled while a doctor probe is running so concurrent IO doesn't race the report shipped inside the bundle. Tooltip rewritten to lead with "Bundle a redacted .zip … share this when filing a bug" so the purpose is obvious without hovering for context.
findBrowser()probe order expanded to Chrome → Edge (all platforms) → Chromium → Brave → override-via-env. Signature{ path, channel: "chrome" | "msedge" | "chromium" | "bundled" }is unchanged; consumers that destructure onlypathare unaffected.resolveBrowserExecutable()return type extended withchannel: BrowserChanneland distinguishessystem-bravevssystem-chromiumin thesourcefield. Old callers that only read{ path }still work.- Error message in
resolveBrowserExecutable()now mentions Edge and Brave as supported options and documents the new env-var names alongside the legacyPERPLEXITY_CHROME_PATH. - Login runners (
login-runner.js,manual-login-runner.js) now passchannelthrough tochromium.launch— previously they only forwarded the path, solaunchfell back to its default behavior for msedge and Brave. buildLaunchOptions()in client.ts switched fromfindChromeExecutable()tofindBrowser()so it can forward the detected channel; the headlesschromium.launchcall acceptschannelwhen one is set.page.evaluatecall sites inclient.tsconverted from string-source to function form (5 sites). The function form gives the bundler/typechecker visibility into the snippet, removes implicit-globals risk, and matches the Patchright API's preferred shape.- Express 4 → 5 alignment in
packages/mcp-server. Direct dep bumped from^4.21.0to^5.2.1to match what@modelcontextprotocol/sdk@1.29.0already pulls in transitively. Drops the dual-install (the daemon's nested express@4 and the SDK's root express@5 were coexisting on the shared(req, res, next)duck-type contract — the source of the 0.6.1req.originalUrlvsreq.pathaudit-log bug). Post-bump npm dedupes to a single express@5.2.1 at the root;npm installremoved 25 nested packages. No source changes were required: every route indaemon/server.tsuses literal paths, no v4-only APIs remain, andhelmet@8.1.0/express-rate-limit@8.xare already v5-compatible.
- "Capturing…" spinner no longer sticks after the diagnostics zip is written. Root cause: packages/extension/src/diagnostics/flow.ts awaited
vscode.window.showInformationMessage(...)before returning the outcome, but VS Code info notifications without a button payload only resolve when the user clicks the X — so the dashboard'saction:resultwas gated on the user dismissing the toast. The success / error notification calls are now fire-and-forget (void Promise.resolve(...).catch(...)), so the outcome returns the moment the file lands on disk. The "Show in folder" action button still works because the underlying VS Code lambda runs to completion in the background even after the flow returns. - Dashboard "Capture diagnostics" toast now offers a "Show in folder" affordance matching the
Perplexity.captureDiagnosticscommand-palette path. Clicking it dispatchesrevealFileInOS(withopenExternalas a fallback) so users can jump straight to the saved zip without copying the path out of the toast. - Doctor probe (Deep check) no longer leaks visible Chrome windows. Two related bugs in packages/mcp-server/src/checks/probe.js: (1)
client.init()was outside the try/finally, so when init threw — most commonly when the headed Cloudflare bootstrap couldn't resolve in 20s, or when the headless launch was killed by AV mid-spawn —client.shutdown()was never called and the spawned chrome.exe (plus its visible window from the headed phase) was leaked; (2) the probe always ran the full headed bootstrap, opening a real visible browser on every Deep check click, so a sequence of failed probes would pile up windows. Fix: scopePERPLEXITY_HEADLESS_ONLY=1around the probe so it never opens a visible window (probe is a smoke test for the headless search path, which is what tools actually use), and moveclient.init()inside the try block so the finally always runsclient.shutdown(). The env var is restored to its prior value in the samefinally, so concurrent tool-call clients are unaffected. Existing dangling Chrome windows from before this fix must be closed manually. - Doctor
browsercheck no longer pops a visible Chrome window on every run on Windows. Root cause: packages/mcp-server/src/checks/browser.js ranchrome.exe --versionto read the version string, but on Windows Chrome forks itself when launched from a non-console parent (the VS Code extension host) — the original returns exit-code 0 with empty stdout (which is exactly why the doctor report'schrome-familymessage was blank), and the forked children stay alive as visible browser windows. EveryrunDoctor()call therefore leaked one window on Windows: clicking Run, Deep check, Capture diagnostics, or Export each opened a new Chrome window that never closed. Fix: on Windows, query the executable'sVersionInfo.ProductVersionviapowershell.exe -NoProfile -NonInteractive -Command "(Get-Item -LiteralPath '<chrome.exe>').VersionInfo.ProductVersion"withwindowsHide: true. PowerShell reads the PE header — same string the user sees in File Properties → Details — without launching the browser. macOS / Linux still use--version(CLI app contract is honored there). Existing windows from before this fix must still be closed manually (taskkill /IM chrome.exe /T), but no new ones will spawn. safeAtomicWriteFileSynchelper + 7 call-site replacements. New helper at packages/mcp-server/src/safe-write.js writes to a${path}.tmpstaging file thenrenameSyncs into place; on Windows that'sMoveFileExW(MOVEFILE_REPLACE_EXISTING), atomic with normSyncwindow. On any failure the.tmpfile is best-effort deleted and the original error rethrown. Replaces seven hand-rolled write+rename pairs acrossdaemon/local-tokens.ts,daemon/oauth-consent-cache.ts,daemon/server.ts,daemon/token.ts,daemon/tunnel-providers/cloudflared-named-setup.ts,daemon/tunnel-providers/index.ts,daemon/tunnel-providers/ngrok-config.ts, andvault.js— eliminates a Windows race where a crash betweenunlinkSync(target)andrenameSync(tmp, target)left no file on disk.clearStaleSingletonLockshelper + integration inclient.ts. New helper at packages/mcp-server/src/fs-utils.js unlinks Chromium'sSingletonLock/SingletonCookie/SingletonSocketfiles from the persistent user-data-dir before launch. Chromium silently exits with code 0 when these files claim an active instance, so a stale lock from an unclean previous exit was breakinglaunchPersistentContexton every restart until the user manually wiped the profile.- Stale-version daemon reaper at packages/extension/src/daemon/stale-version.ts (
isLockStale,removeStaleLock,killStaleDaemonPid) wired into daemon/runtime.ts. When the extension activates against a daemon launched by an older bundled version, the helper SIGTERMs that pid and removes the lock so a fresh daemon can spawn. Older ESM module graphs pin hashed chunk filenames at startup; later upgrades overwrite those files on disk and dynamic imports for code-split chunks (e.g.perplexity_doctor'sdoctor-XXXXX.mjs) fail forever. Any version difference (newer or older) is treated as "stale". getClient()init-rejection retry in packages/mcp-server/src/daemon/server.ts. When a cached client'sinit()promise rejects, the cache is now invalidated so the nextgetClient()call constructs a fresh client instead of returning the rejected promise forever.
- Obscura runtime support (briefly attempted on this branch). The h4ckf0r0day/obscura CDP server didn't expose the
Target.createTarget/ frame-attachment domains Patchright relies on, so the connect-over-CDP path could never bootstrap a usable session against it. All Obscura plumbing —ObscuraManager,connectOverCDPbranches in client.ts / refresh.ts / health-check.js, theobscurachannel inBrowserChannel, thePERPLEXITY_OBSCURA_ENDPOINTenv var, and theobscurabrowser-icon — was ripped out. The migration shim that downgraded a savedbrowserChoice.channel === "obscura"tomode: "auto"was also removed since no released build ever shipped that channel.
- New browser-detect / download-manager modules ship without dedicated tests in this commit. Follow-up work: unit tests for
AuthManager.resolveBrowserEnv(table-driven). safe-write.test.jscovers happy path, write-failure cleanup, AND a new rename-failure case wherewriteFileSync(tmp)succeeds butrenameSync(tmp, target)fails because target is an existing directory — asserts the original target is preserved and the.tmpstaging file is cleaned up.fs-utils.test.jscoversclearStaleSingletonLockshappy / partial / missing-dir paths.getClient-retry.test.jsinpackages/mcp-server/test/daemon/covers the init-rejection re-creation path — firstgetClient()gets a rejected client, second call returns a fresh one.stale-version.test.tsinpackages/extension/tests/coversisLockStale/removeStaleLock/killStaleDaemonPidincluding ESRCH and EPERM error-code handling.recordLoginSuccesscoverage inprofiles.test.js— three focused tests bringingprofiles.jsfunction coverage from 83.33% to 87.5%, restoring the 85% per-file floor enforced byvitest.config.ts.reauth-cycle.integration.test.jsreplaces a fixed 400ms wall-clock wait with condition polling up to a 5s deadline; under parallel vitest workers, event-loop pressure plus the watcher's 200ms debounce occasionally exceeded 400ms and produced false-negativereinitFired === 0assertions.
- Windows CI matrix. .github/workflows/ci.yml extended from
{ubuntu-latest} × {node 20, 22}to{ubuntu-latest, windows-latest} × {node 20, 22}.shell: bashpinned at the job level (windows-latest ships Git Bash preinstalled),fail-fast: falseso a Windows-specific failure doesn't cancel the Linux legs. Surfaces the Windows-specific code paths (atomic-rename behavior, icacls token ACLs, backslash path handling, the newsafeAtomicWriteFileSynchelper) that were never exercised in CI before.
- Lockfile workspace-version sync.
package-lock.jsonmirror copies updated to track the four workspacepackage.jsonbumps from this cycle (extension/mcp-server: 0.8.5 → 0.8.10, shared: 0.1.0 → 0.1.2, webview: 0.1.3 → 0.1.5). Pure metadata sync — no dependency tree changes, no integrity-hash churn, no transitive resolution differences.
- Typecheck: green across all 4 packages.
- Full suite: 109 files / 942 pass + 2 skip (no failures); per-file coverage thresholds clear after
recordLoginSuccesscoverage was restored. npm audit --audit-level=high: exit 0 (5 moderate remain in the postcss + uuid-via-vsce chain, out of scope).
[0.8.5] — 2026-04-24 — UX polish: auto-regen + tunnel switching safety + perf dashboard + loopback-default
v0.8.4's smoke surfaced three UX gaps and two user-preference shifts: staleness never auto-healed, tunnel provider switches had no warning or performance visibility, and the tunnel UI was always-visible even for users who only use loopback. This release addresses all five, in that order.
- Auto-regenerate stale MCP configs. New setting
Perplexity.autoRegenerateStaleConfigs(default true). When a daemon restart (port drift) or tunnel-URL rotation leaves a configured IDE'smcp.jsonpointing at dead values, the extension now automatically re-runsapplyIdeConfigfor each stale IDE withconfirmTransportforced totrue(this is a refresh, not a first-time write — the H5 intent was to guard surprise writes; the user already approved this (IDE, transport) pair).warnSyncFolderis preserved by reference so the sync-folder gate still fires. Audit entries for auto-regen writes carryauto=trueto distinguish them from user-driven generates. Pure-function core atpackages/extension/src/webview/staleness-auto-regen.tsso tests don't need any VS Code mock. - Staleness pipeline observability. Pre/post-detection traces on
postStaleness:[staleness] checking <N> ides against daemonPort=<P> tunnelUrl=<U>before detection and[staleness] posted <N> stale config(s): <tag>(<reason>), ...after thetransport:stalenessmessage is sent. Makes it possible to grep the Output channel to prove the pipeline ran + what it found. - Tunnel-switch confirmation modal. Before
daemon:set-tunnel-providertakes effect, a VS Code warning modal with both the current and next provider names, explaining that the current tunnel will disconnect, any MCP client connected through the current URL will drop, and any IDE configured forhttp-tunnelwill need regenerating.http-loopbackand stdio IDEs are unaffected is called out explicitly so the user knows the disruption is bounded. Default-confirm ("Continue switching"primary,"Cancel"secondary). Skipped when no tunnel is currently enabled OR the user re-selects the same provider (idempotent no-op). Pure helper atpackages/extension/src/webview/tunnel-switch-confirm.ts— fully unit-tested, no VS Code mock needed. After the switch completes,postStalenessfires immediately so the banner reflects the new tunnel URL on next render. - Tunnel performance dashboard. New
TunnelPerformancecomponent in the TunnelManager card shows:- Last enable durations per provider (session-local ring buffer; cf-named ~1.5s, ngrok ~2s, cf-quick ~5.5s observed in testing — visible now).
- Rolling average health-check latency over the last 10
/daemon/healthloopback hits from the audit tail. - MCP
/mcpstatus ratios by source (loopback vs tunnel) over the last 200 audit entries:ok / unauthorized / serverError / other. - High-401 warning hint when tunnel unauthorized ratio exceeds 10%: directly surfaces CF WAF / OAuth misconfigurations without the user having to read audit logs.
Data pipeline:
parseTunnelPerformancepure parser inpackages/extension/src/webview/tunnel-performance.ts+ session-localTunnelEnableRecorderring buffer intunnel-enable-recorder.ts(extension-host scoped, resets on reload; provider ids + wall-clock ms only). Newtunnel:performanceoutbound message + store slice. Renders nothing pre-hydrate; graceful empty-state for every sub-section.
- Loopback-default mode + tunnels opt-in. New setting
Perplexity.enableTunnels(default false). When disabled, TunnelManager collapses to a singleRemoteAccessOptIncard explaining what a tunnel is for and a single "Enable tunnel options" button.http-tunnelis also hidden from every IDE's TransportPicker (not just disabled — removed from the rendered radio group). When enabled, the full tunnel UI returns with a "Disable tunnel options" link at the bottom that fires a confirm modal and tears down any active tunnel atomically before flipping the setting. - Migration for existing users. On first activation of 0.8.5,
migrateEnableTunnelsOncechecks for an existing<configDir>/tunnel-settings.jsonwith a non-emptyactiveProvider— if found AND the user hasn't explicitly setenableTunnelsyet, the setting is auto-set totrueso upgraders keep their familiar UI. One-shot, flagged byglobalState.perplexity.enableTunnels.migrated.
packages/mcp-server+packages/extensionbump to0.8.5.DashboardProvider.postDaemonStatenow runs a 3-step downstream chain after posting status:postStaleness → auto-regen if enabled → postTunnelPerformance. Each step is independently traced and individually try/catch-guarded so one failure doesn't poison the others.TransportPickergains atunnelsEnabledprop;TunnelManagergains asettingsprop;DaemonStatusthreads both from the store.settings:updatehandler interceptsenableTunnels: falseto run the disable confirmation + tunnel shutdown flow before writing the setting. Co-sent keys in the same payload are still applied (the interceptor strips onlyenableTunnelsfrom the partial if the user cancels).
- No new secrets surface. The static daemon bearer remains loopback-only (H11 from v0.7.4); auto-regen uses the same
getDaemonBearerdep as manual generate. Sync-folder detection (H4) is preserved on the auto-regen path — an IDE whosemcp.jsonlives under OneDrive/Dropbox/etc. still triggers the default-deny modal, even during a silent refresh. - Enable-history and health-latency metrics stored in-memory only; never persisted to disk; no plaintext tokens touched.
- 805 passed / 94 files — up from 742 / 86 at v0.8.4 (+63 new tests across the patch). Breakdown: 12 staleness-auto-regen (Wave 1), 7 tunnel-switch-confirm (Wave 2α), 29 tunnel-performance + recorder + component (Wave 2β), 15 loopback-default + migration + opt-in + picker-filter (Wave 3).
- Typecheck: green across all 4 packages.
- Full suite: 805 passed / 94 files.
- Secret-leak gate: clean on
.test-artifacts/vitest.log.
v0.8.3 shipped the transport picker UI + dispatcher but the wire between the two was severed — HTTP transport options looked clickable and landed in the picker but never persisted, and the capability matrix kept every HTTP option disabled for every IDE. Owner's smoke produced a diagnostics zip showing mcpTransportByIde: {} after picking HTTP in the UI and every audit line defaulting to stdio-daemon-proxy. This release closes the five gaps surfaced by that smoke.
http-loopbackstatic-bearer variant. NewbearerKind: "static"onTransportBuildInput;http-loopbackbuilder embedsAuthorization: Bearer <daemon-static-bearer>when this kind is set. The daemon's static bearer is already accepted on loopback via Phase 8.2 H11's source-awareverifyAccessToken, so this transport now works out of the box for every IDE the picker can reach. Per-client scoped bearers (thelocal-tokens.tsprimitives shipped in 8.6.1) remain for future work — the dispatcher'sbearerKinddecision now prefers"static"whenhttpBearerLoopbackis the capability, and the"local"branch is unreachable until a future evidence-gated capability flip re-enables it.- Capability baseline.
IdeMeta.capabilities.httpBearerLoopbackflippedtruefor every auto-configurable JSON IDE (cursor,claudeDesktop,claudeCode,cline,windsurf,windsurfNext,amp,rooCode,continueDev,zed). Evidence file:docs/smoke-evidence/2026-04-24-http-loopback-static-bearer.md.httpOAuthLoopbackandhttpOAuthTunnelremainfalseeverywhere — those require the OAuth-discovery + RFC 8707 resource-binding evidence paths which are separate future work. getDaemonBearerdependency onApplyIdeConfigDeps. Wired inbuildApplyIdeConfigDepsLiveto readstatus.record.bearerTokenfrom the bundled daemon lockfile. Unit tests cover the error paths (null bearer →ok: false, reason: "error"; dep not provided → same shape).transport:select+transport:regenerate-staleextension-host handlers inDashboardProvider. The first persists the user's per-IDE choice toPerplexity.mcpTransportByIdeviavscode.workspace.getConfiguration().update(...), then re-posts the settings snapshot so the webview re-renders with the committed value. The second delegates to the existingPerplexity.generateConfigscommand so both entry points (dashboard button and command palette) share the same modal + capability gates. Unit-testable via a new extractedtransport-select-handler.tshelper following the repo'sbearer-reveal-gate.tspattern.- Staleness detector at
packages/extension/src/webview/staleness-detector.ts. Pure function that reads each auto-configurable IDE's mcp.json, extracts the Perplexity entry, and comparesurl/headers.Authorizationagainst the live daemon port + tunnel URL + static bearer. Returns{ ideTag, reason: "bearer" | "url" }[]. Skips unreadable or malformed configs silently per IDE. Wired intoDashboardProvider.postDaemonStateso every daemon-state push triggers a fresh comparison; the result flows to the webview viatransport:staleness, populating thestaleConfigsslice added in 8.6.5 and finally making the "N configs contain stale auth" banner visible. - Surfaced error modals in
Perplexity.generateConfigs. The command handler now inspectsoutcome.results, groups failures, builds a reason-specific message per target, and surfaces viawindow.showErrorMessagewith an "Open Output" action that reveals the Perplexity output channel. Coversunsupported,sync-folder,tunnel-unstable,port-unavailable,cancelled,errorreasons. Previous behaviour: audit-only, silent to the user. - Cloudflare Named Tunnel WAF warning banner in the dashboard's TunnelManager card. Fires when
activeProvider === "cf-named"ANDtunnel.status === "enabled". Text: the tunnel URL inline, an inlinePath = "/mcp"WAF-Skip-rule recipe, and a link tohttps://developers.cloudflare.com/waf/custom-rules/skip/. Not dismissable — the warning corresponds to a one-time Zone configuration; future "tested & working" state collapse is a deferred enhancement.
packages/mcp-server+packages/extensionbump to0.8.4.ApplyIdeConfigResultaudit line shapes expanded with the new"static"bearer-kind tag. The surfaced toast includes failure detail aggregated across targets; successful generates still post an info notice to the dashboard.
- No design regression. The static daemon bearer is now embedded in IDE
mcp.jsonfiles only when the capability matrix allows http-loopback, and only for loopback URLs (http://127.0.0.1:<port>/mcp). The daemon has rejected static bearers over tunnel since Phase 8.2 H11; that invariant is unchanged here.http-tunnelcontinues to emit{ url }only — noheaderskey, ever — regardless ofbearerKind. - H4 sync-folder detection now also guards the new
"static"bearer path (previously only"local"triggered it). If a user picks http-loopback for an IDE whose mcp.json lives under a sync folder, the same modal + default-deny still fires.
- 742 passed / 86 files — up from 705 / 83 at v0.8.3 (+37 tests across the patch). Breakdown: 7 http-loopback static-variant + 2 apply-ide-config static-branch error paths (Wave 1), 14 staleness-detector + 7 transport-select-handler + 2 apply-ide-config additional (Wave 2α), 7 cf-named WAF banner (Wave 2γ).
- Typecheck: green across all 4 packages.
- Full suite: 742 passed / 86 files.
- Secret-leak gate: clean on
.test-artifacts/vitest.log.
Per-IDE choice between four MCP transports — stdio-in-process, stdio-daemon-proxy (default), http-loopback, http-tunnel — with capability-gated availability, security prechecks (H3–H8 from the 8.6 design), and a dashboard picker UI. All HTTP capability flags start false across every IDE; flipping one to true requires a dated docs/smoke-evidence/*.md file plus a committed generator golden fixture. As of 0.8.3 every IDE ships stdio-only — the contract, UI, and dispatcher are in place for individual HTTP capabilities to flip as evidence lands.
- Four transport builders at
packages/extension/src/auto-config/transports/. Pure-functionbuild(input): McpServerEntry; sharedTransportBuilder/TransportBuildInputtypes,UnsupportedTransportError+StabilityGateErrorclasses, and agetTransportBuilder(id)registry.http-tunnelemits{url}only — never aheaderskey, even when called withbearerKind: "local"(defense in depth against config leaking a bearer to a public URL).http-loopbacksupports both OAuth (headerless) and scoped-bearer-fallback variants.stdio-daemon-proxy(noPERPLEXITY_NO_DAEMONenv var) is the shipped default. McpTransportId,MCP_TRANSPORT_DEFAULT,MCP_TRANSPORT_IDS,IdeCapabilitiesexported from@perplexity-user-mcp/shared.IdeMeta.capabilitiespopulated for every IDE.- Three new settings (
Perplexity.mcpTransportByIde,Perplexity.daemonPort,Perplexity.syncFolderPatterns) + one new command (Perplexity.regenerateStaleConfigs). - Scoped local-bearer primitives at
packages/mcp-server/src/daemon/local-tokens.ts:issueLocalToken/verifyLocalToken/revokeLocalToken/listLocalTokens. Hash-at-rest at<configDir>/local-tokens.json(0600); plaintext returned once on issuance; constant-time compare viacrypto.timingSafeEqual; revoked entries are returned bylistbut short-circuited inverify.revokeandlistpropagate disk I/O errors (control-plane paths);verifyswallows them and returns null (fail-closed) so the auth hot-path can't crash.lastUsedAtwrite-back failures log-and-proceed rather than DOS the verify path. Token format:pplx_local_<ide-sanitized>_<base64url24>. Metadata id:local-<ide>-<base64url8>. applyIdeConfigdispatch rewrite with a structuredApplyIdeConfigResultdiscriminated union andApplyIdeConfigDepsdependency injection for VS Code modals, git-tracked detection, audit sinks, and daemon-state readers. 11-step pipeline: capability gate → format gate → H4 sync-folder detect (only for http-loopback bearer branch) → H5 confirmation modal (workspaceState-remembered per(ideTag, transportId)pair) → bearer-fate decision → H6 port-pin nudge → local-token issuance → builder.build (H7 stability gates delegated to the http-tunnel builder) → H3 sanitized.bak(stripbearerToken/token/secret/Authorizationkeys +pplx_*/"Bearer "string values →"<redacted>", write 0600, atomic rename, restore + delete on failure, unconditional delete on success) → H8 audit → return.removeIdeConfiggot the same.bakhygiene.writeJsonAtomic+writeTextAtomicnow open tempfiles 0600 AND callapplyPrivatePermissionsbefore rename so the pre-rename window is not world-readable.- TransportPicker (
packages/webview/src/components/TransportPicker.tsx) — radio group per IDE row, capability-gated disabled states with inline reasons, emitstransport:select. Rendered inside every auto-configurableIdeCard. - BearerReveal (
packages/webview/src/components/BearerReveal.tsx) — extracted the 30s-TTL bearer-reveal row from DaemonStatus into a dedicated component. Props-controlled; returns null whenavailable === false. - Stale-config banner in the IDEs tab: when the store's
staleConfigsslice is non-empty, shows"N config(s) contain(s) stale auth"with a Regenerate all button dispatchingtransport:regenerate-stale. Per-IDEStalechip in each affected IdeCard header. Slice is hydrated bytransport:stalenessmessages from the extension host;null= pre-hydrate,[]= explicit zero-signal. - Command palette entries.
Perplexity.copyDaemonBearer,Perplexity.showDaemonBearer, andPerplexity.regenerateStaleConfigsall reachable from the Command Palette. Phase 8.6.6 is fully covered by prior work — the first two shipped in 8.2 (v0.7.4) and the third in 8.6.2 (this release).
IdeConfigOptionsgainstransportId?: McpTransportId(defaults toMCP_TRANSPORT_DEFAULT).configureTargetsis async and returns{ statuses, results }instead of just statuses.DaemonStatus.tsxno longer inlines the bearer-reveal UI — it renders<BearerReveal>and keeps the state + TTL tick logic.packages/mcp-server+packages/extensionbump to0.8.3.
- H3 —
.bakcan no longer harbor a bearer across a rotation. Applies to bothapplyIdeConfigandremoveIdeConfig. Tempfile mode tightened to 0600 POSIX / icacls-restricted Windows so no write-then-rename window exposes the secret. - H4 — sync-folder detection fires a modal before any write that embeds a bearer. Well-known dirs (iCloud, OneDrive, Dropbox, Google Drive, Syncthing, pCloud), git-tracked trees (graceful fallback if git is missing), and user-supplied
Perplexity.syncFolderPatternsregexes.http-tunneland OAuthhttp-loopbackare exempt because no secret is written. - H7 —
http-tunnelgeneration rejectscf-quick(ephemeral URL) andngrok-without-reserved-domain via the builder'sStabilityGateError. Error reasons never include the tunnel URL. - H8 — audit line fires on every exit path:
ok,rejected-unsupported,rejected-sync,rejected-cancelled,rejected-tunnel-unstable,rejected-port-unavailable,error.configPathis home-redacted (~/...).
- 705 passed / 83 files — up from 581 / 71 at the start of Phase 8.6 (+124 new tests across the sub-phase). Highlights: 14 local-tokens tests (incl. constant-time compare spy + malformed-file resilience + strict-revoke error propagation + resilient verify write-back), 45 transport builder tests (every gate path, URL normalization including the
/mcp/double-append regression fix, headerless http-tunnel invariant), 18applyIdeConfigtests (every H3–H8 path including sanitized-bak rollback, removeIdeConfig sanitized-bak, bearer-file 0600 mode), 22 UI tests (11 TransportPicker + 11 BearerReveal), 9 staleness-store + banner tests.
- Typecheck: green across all 4 packages.
- Full suite: 705 passed / 83 files.
- Secret-leak gate: clean on
.test-artifacts/vitest.log.
Perplexity.captureDiagnosticscommand + dashboard button. One-click diagnostics bundle for bug reports. Shows a save dialog defaulted to~/Downloads/perplexity-mcp-diagnostics-<ISO>.zip, then writes a redacted zip containing: the extension output channel (last 5000 lines via a newOutputRingBuffer), the daemon log, the last 1000 lines ofaudit.log, an inlinerunDoctorreport,daemon.lock.json(bearer scrubbed),tunnel-settings.json,oauth-clients.json, apackage-versions.jsonmanifest, andREDACTION_NOTES.mdexplaining what was scrubbed. The "Show in folder" button on the success toast opens the enclosing directory viarevealFileInOS. Dashboard button sits in the daemon card alongside the existing kill/restart actions.packages/extension/src/diagnostics/capture.ts. Pure-functioncaptureDiagnostics({ outputPath, configDir, extensionVersion, vscodeVersion, logsText?, doctorReport?, now?, fs? }): Promise<CaptureResult>. Single atomic write; bundles throughjszip(bundled intodist/extension.js, not shipped as a separate tree). All file reads dependency-injected for test hermeticity; all content exceptpackage-versions.jsonpasses through the diagnostics redactor.packages/extension/src/diagnostics/redact.ts. Wraps the existing extension/server redactors with a PEM-block layer (/-----BEGIN <TYPE>-----…-----END <TYPE>-----/gwith a backreferenced type token so adjacent blocks don't merge; PEM-first so cert bodies aren't half-eaten by the generic long-token rule). ExportsredactDiagnosticsString/redactDiagnosticsObject.OutputRingBufferatpackages/extension/src/diagnostics/output-buffer.ts. 5000-line ring;log()anddebug()tee every line into it after the existingredactMessagepass so snapshots are already scrubbed. Exposed viagetOutputRingBuffer()forcaptureDiagnosticsconsumers.- Shared DI flow helper at
packages/extension/src/diagnostics/flow.ts. Same pattern aswebview/bearer-reveal-gate.ts— onerunDiagnosticsCaptureFlowdrives both the command-palette entry and the dashboard message handler so save-dialog / doctor-probe / zip-write / result-post logic lives in one unit-testable place. Returns a discriminatedDiagnosticsFlowOutcomeso callers can signal spinner state correctly.
- Legacy
debugCollectorinfrastructure. Fully superseded by the unified capture path. Deleted:packages/extension/src/debug/(collector, exporter, instrumentation, stderr-parser),packages/shared/src/debug.tsand its re-export, thePerplexity Debug Traceoutput channel, thedashboard.setDebugCollectorwiring, and the three commandsPerplexity.debugStartSession/Perplexity.debugStopAndExport/Perplexity.debugExportAll. Removed settings:Perplexity.debugBufferSize(the new ring buffer is not user-configurable; 5000 lines is enough for a diagnostics snapshot and the ring never grows).DashboardState.debugremoved from the shared contract.
DashboardProviderroutesdiagnostics:captureinbound messages to the same flow helper the command uses; posts the typeddiagnostics:capture:result+ maps outcome kind topostActionResultso the webview pending-action spinner releases on every outcome (ok / cancelled / error / throw).- Webview-side message contract: inbound
{ type: "diagnostics:capture"; id: string }onWebviewMessage; outbounddiagnostics:capture:resultdiscriminated union onExtensionMessage.ACTION_TYPESregisters the inbound so correlation-id tracking clears correctly. packages/mcp-server+packages/extensionbump to0.8.2.
- Added
jszip ^3.10.1as a bundled dep (placed indevDependenciesper the project's convention that runtime deps bundled intodist/extension.jslive there, alongsidepatchrightet al.;prepare-package-deps.mjs's hardcodedrootPackageslist controls what ships indist/node_modules/and is unchanged).
- 581 passed / 71 files (up from 539 / 65 at the start of Phase 8.5; +42 new tests across 8.5.1/8.5.2; 8.5.3 was pure deletion — zero tests existed for the removed legacy debug infra). Breakdown: diagnostics-redact 11 (PEM variants + ordering + nested objects), diagnostics-capture 8 (happy path with 9 zip entries + missing-file markers + 1500→1000 audit tail + bearer scrub in daemon.lock + package-versions never-redacted + malformed-lockfile parse-error entry + PEM-in-tunnel-settings + bytesWritten matches
stat.size), output-buffer 6, diagnostics-command 6, DashboardProvider.diagnostics 8 (baseline 4 + outcome-signalling 4: error →postActionResult(false), cancel →postActionResult(true), happy →postActionResult(true), showSaveDialog throw → outer catch releases spinner), DaemonStatus.diagnostics 3 (button renders + click sends message + action-type registered).
- Typecheck: green across all 4 packages.
- Full suite: 581 passed / 71 files.
- Cloudflare Named Tunnel provider (
cf-named). Third tunnel option alongsidecf-quick(Cloudflare Quick Tunnels) andngrok. Targets users with a Cloudflare-managed domain: one-timecloudflared tunnel loginwrites~/.cloudflared/cert.pem, then the dashboard walks through creating a named tunnel, installing a DNS CNAME on the user-chosen<sub>.<zone>, and writing a managed YAML config at<configDir>/cloudflared-named.yml(0600). Persistent URL, free Cloudflare Access OAuth + WAF + logs on top. - Setup helpers (
daemon/tunnel-providers/cloudflared-named-setup.ts). All spawn-based, injectable via the existingdependencies.spawnDI pattern — zero new npm deps.runCloudflaredLoginpolls for the cert file on a 250ms tick (cloudflared login doesn't always exit cleanly after emitting the URL) and rejects up front if a cert already exists so we never spawn a stale-account login flow.createNamedTunnelparsesTunnel credentials written to <path>.jsonwith a\.json-anchored regex so cloudflared's same-line advisory prose doesn't pollute the captured credentials path.writeTunnelConfiguses the tempfile + rename + icacls/chmod pattern fromngrok-config.tsso the managed YAML (references sensitive credentials) lands 0600 atomically. - Dashboard setup widget. New missing-binary / missing-cert / missing-config / missing-credentials / ready states, each with distinct recovery actions (install binary, run cloudflared login, create-new / bind-existing / list-existing forms). The missing-credentials state explicitly offers recovery rather than dead-ending at a red banner — user feedback during smoke showed corrupted
<uuid>.jsonpointers hitting a click-nothing screen in the first pass; 843759b added recovery forms + explicit copy naming the managed YAML path. - CLI mirrors.
perplexity-user-mcp daemon cf-named-login/cf-named-create/cf-named-list/cf-named-install/cf-named-delete/cf-named-unbind. Each wraps the same runtime helpers the dashboard uses, forwards cloudflared's stderr + stdout to parent stderr so the browser-login URL is visible in the terminal, and preserves stdout JSON discipline. - Message transport. Shared contracts for
daemon:install-cloudflared,daemon:cf-named-login,daemon:cf-named-create,daemon:cf-named-list,daemon:cf-named-delete,daemon:cf-named-unbind. ActionTypes now stamps correlation ids on all six so pending-action tracking clears correctly when…:resultfires.
start()rewrites the managed YAML with the current daemon port on every invocation. The daemon uses OS-assigned ports and picks a different one on almost every restart, so the persisted port is nearly always stale. Missing this rewrite would route cloudflared to a dead port on each reconnection; idempotent writes are cheap.deriveCfNamedStatechecks credentials-file-not-found before any cert keyword. The earlier ordering routed a corrupted-credentials-path error containing the substring "origin certificate" tomissing-cert, putting the UI into a login-button loop (login rejected because cert existed → UI unchanged). Specific-state-wins ordering plus tightening the cert alternation to the exact phrase "origin cert not found" closes that loop.TunnelProviderIdextended from"cf-quick" | "ngrok"to include"cf-named". Provider registry gains the new entry.packages/mcp-server+packages/extensionbump to0.8.1.
- 539 passed / 65 files — up from 433 / 60 at the start of Phase 8.4 (+106 new tests). Breakdown: mcp-server daemon (setup helpers + provider lifecycle + port-drift + login flow + CLI paths) ~60; extension dashboard (
DashboardProvider.cf-named.test.ts) + ActionTypes pin ~10; webview (DaemonStatus.cf-named.test.tsx+DaemonStatus.test.tsxstate-machine regressions + AuthorizedClients harness update) ~36.
- Typecheck: green across all 4 packages.
- Full suite: 539 passed / 65 files (32.88s).
attachToDaemonprogrammatic API re-exported fromperplexity-user-mcp's main entrypoint. The bundleddist/mcp/server.mjsnow exposes it so the extension launcher can reach it via thebundled-path.jsonindirection without adding a CLI child-process.--fallback-stdioand--ensure-timeout-ms <N>flags ondaemon attach. When--fallback-stdiois set and the daemon cannot be reached within the ensure-timeout, the CLI writes a single stderr warning ([perplexity-mcp] daemon unreachable (...); falling back to in-process stdio) and drops through to the in-process stdiomain()so the client still gets a working server.PERPLEXITY_NO_DAEMONenv opt-out. When set to1/true(case-insensitive, trimmed), both the stdio launcher ANDdaemon attachCLI bypass the daemon and run a pure in-process stdio server. Warning goes to stderr only (stdout is reserved for MCP JSON-RPC framing).
- Generated stdio launcher (
~/.perplexity-mcp/start.mjs) now multiplexes every external stdio MCP client (Claude Desktop, Cursor, Cline, Codex CLI, Amp, …) onto the shared daemon viaattachToDaemon({ fallbackStdio: true }). Pre-0.8.0, each client spawned its own in-process stdio server + Chromium. Post-0.8.0, N clients = 1 daemon + 1 Chromium. The launcher passes arunStdioMainDI shim pointing at the already-loadedserver.mainso the fallback path works correctly in the extension-bundled layout (whereattach.tsis inlined intoserver.mjsand the defaultimport("../index.js")would resolve to a nonexistent sibling). ensureLaunchernow force-updates stalestart.mjscontent. Byte-for-byte comparison + rewrite on mismatch. Users upgrading from 0.7.x will automatically migrate to the new daemon-proxy launcher on next activation, without needing to reinstall the extension.packages/mcp-server+packages/extensionbump to0.8.0.
- 13 new tests across the phase (8.3.1–8.3.3): 2 in
packages/mcp-server/test/daemon/attach.test.js(fallback-stdio path + hard-failure preservation when fallback is disabled), 5 inpackages/mcp-server/test/cli.test.js(PERPLEXITY_NO_DAEMON opt-out contract + stdout discipline), 6 inpackages/extension/tests/write-launcher.test.ts(launcher content shape + force-migration from 0.7.x). Total: 433 passed / 60 files (up from 420 / 59 at start of Phase 8.3).
- Typecheck: green across all 4 packages.
- Full suite: 433 passed / 60 files.
- VSIX:
packages/extension/perplexity-vscode-0.8.0.vsix, ~11.7 MB (12,252,644 bytes), 3419 files.grep -c "attachToDaemon" dist/mcp/server.mjs= 2 (confirms the re-export reached the bundle).
- H0 — Closed live bearer-in-logs leak.
DaemonStatusState.bearerTokenis removed; replaced withbearerAvailable: boolean. The webview never receives the raw daemon bearer on state pushes. New explicit one-shot channelsdaemon:bearer:copy(extension-host clipboard write; bearer never touches the webview) anddaemon:bearer:reveal(modal-confirmed 30s-TTL reveal with nonce). A redactor now wraps all log sinks (extensionlog/debug, thelog:webviewforwarder, and daemon[trace]paths). New CI gatescripts/assert-no-secret-leak.mjsscans captured test logs for known secret shapes — zero hits required per release. - H11 — Admin surface locked to loopback. Every
/daemon/*endpoint now returns404 Not Foundto tunnel callers regardless of bearer validity. Tunnel path allowlist:/mcp,/,/authorize,/token,/register,/revoke,/.well-known/{oauth-authorization-server,oauth-protected-resource},/robots.txt,/favicon.ico. NewattachRequestSourcemiddleware derivesloopbackvstunnelfrom real network indicators only (X-Forwarded-For,CF-Connecting-IP,req.ip); thex-perplexity-sourceheader is still captured for audit enrichment but is never consulted for security decisions. - H12 — RFC 8707 resource binding. OAuth tokens now carry a
resourcebinding captured at/authorize, validated on both code- and refresh-grant exchanges at/token, and enforced at/mcp. The static daemon bearer is loopback-only. The tunnel rejects OAuth tokens whose bound resource mismatches the incoming request AND tokens with no bound resource; the loopback path accepts unbound tokens (taggedoauth-unbound) strictly for migration of pre-0.7.4 clients. SDK-aligned signatures:exchangeAuthorizationCode(client, code, codeVerifier?, redirectUri?, resource?)andexchangeRefreshToken(client, refreshToken, scopes?, resource?). One canonicalresolveRequestResource(req)helper is used by PRM, code/token binding, and/mcpverification. Consent cache keys now include the resource so a client that re-authorizes against a different tunnel URL re-prompts.
- Authorized OAuth clients dashboard panel. A new card below daemon status shows every client registered via
/registerwith its client ID, last-used timestamp, consent approval timestamp, and active token count. Per-row Revoke (modal confirm, invalidates all outstanding tokens for that client) and a card-level Revoke all (modal lists every affected client). Local-bearer rows land in Phase 8.6. Perplexity: Copy Daemon Bearercommand. Modal-confirm, thenvscode.env.clipboard.writeTexton the extension host — the bearer never leaves the host process.Perplexity: Show Daemon Bearer (30s)command. Modal-confirm, then a one-shot reveal to the dashboard with a 30s TTL and auto-clear./daemon/oauth-clientsendpoints.GETlists authorized clients;DELETErevokes byclientIdor wipes all. Static-bearer gated and loopback-only per H11.- H12 follow-up: consent-binding. Cached consents are now keyed by
(client_id, redirect_uri, resource)so a client re-authorizing against a different tunnel URL re-prompts the modal instead of inheriting the prior consent. scripts/assert-no-secret-leak.mjs. Node (Windows-first) CI gate that scans captured test logs for known secret shapes plus env-provided canary values.
packages/mcp-server+packages/extensionbump to0.7.4.AuditEntry.authunion gainsoauth-cached(Phase 8.1) andoauth-unbound(Phase 8.2) — cached-consent approvals and legacy unbound-token loopback paths are now distinguishable in audit lines.DaemonStatusStateshape change:bearerToken: string | nullremoved,bearerAvailable: booleanadded. All webview consumers updated; the bearer is requested explicitly via the new copy / reveal channels.
- 179 passed (29 files) — up from 43 at the start of Phase 8.2; 136 new tests across the phase. Breakdown: daemon + OAuth conformance + resource binding + consent-cache + tunnel allowlist + admin endpoints (~122), extension-host redaction + bearer-reveal + auto-config + auth-manager + history + doctor (~43), webview AuthorizedClients panel + DaemonStatus bearer-reveal TTL + ActionTypes pin (~14).
- Pre-0.7.4 OAuth tokens minted without a
resourcebinding are rejected over the tunnel post-upgrade. Legacy external clients (Claude Desktop / Cursor / Cline connected over the tunnel) must re-authorize and include an RFC 8707resourceparameter at/authorize. Loopback callers are unaffected — the daemon accepts unbound tokens on127.0.0.1strictly for migration and tags themoauth-unboundin audit.
- OAuth consent cache. The daemon now remembers per-(client_id, redirect_uri) consents so Claude Desktop / Cursor / Cline don't re-prompt a VS Code modal on every ~1h token-refresh cycle. Cache lives at
<configDir>/oauth-consent.json(0600). Default TTL 24h, configurable via the newPerplexity.oauthConsentCacheTtlHourssetting.0disables the cache (modal every time); max 720h (30d). - Admin endpoints for inspecting and clearing the cache:
GET /daemon/oauth-consentsreturns{ consents: [{ clientId, redirectUri, approvedAt, expiresAt }] }.DELETE /daemon/oauth-consentsrevokes by body{ clientId, redirectUri? }; empty body revokes everything. Returns{ ok, removed }. Static-bearer gated only (no OAuth-token path) so no OAuth client can inspect or wipe another's consents.
- Launcher helpers
listOAuthConsents,revokeOAuthConsent,revokeAllOAuthConsents(new subpath exportperplexity-user-mcp/daemon) plus matchinglistBundledOAuthConsents/revokeBundledOAuthConsent/revokeAllBundledOAuthConsentson the extension runtime. - Dashboard message transport wired for a future 8.2 UI panel —
daemon:oauth-consents-list,daemon:oauth-consents-revoke,daemon:oauth-consents-revoke-allinbound,daemon:oauth-consentsoutbound.
PerplexityOAuthProvider.revokeClientnow also purges that client's cached consents so a future re-registration with the sameclient_idcan't silently inherit stale approvals.AuditEntry.authgainsoauth-cached— audit lines for cache-driven auto-approvals are distinguishable from both unauthenticated and fresh-modal approvals.ExtensionSettingsSnapshotgainsoauthConsentCacheTtlHoursfor future UI surfacing.packages/mcp-server+packages/extensionbump to0.7.3.
- Extension reads the setting and writes
PERPLEXITY_OAUTH_CONSENT_TTL_HOURSinto the daemon spawn env. The provider reads it live per/authorizeso toggling the setting takes effect on the next OAuth handshake without a full daemon restart. - On cache hit:
authorize()skips the consent modal, logs[trace] oauth consent cache hit clientId=… redirectUri=…, firesonConsentCacheHitsoserver.tscan flip the audit tag, and issues the authorization code. - On fresh approval (cache miss + user approves): cache entry written with the current TTL.
- On denial: cache is NOT written.
- When the tunnel error row shows an
ERR_NGROK_334message (reserved-domain server-side lockout), two new buttons now appear inline:- Try ephemeral URL — clears the saved ngrok domain and retries Enable immediately. Use this to unstick the tunnel without losing your authtoken.
- Open ngrok endpoints page — direct link to
dashboard.ngrok.com/endpoints(the authoritative page for account-level endpoint state, distinct from the Tunnels page which often shows empty while the endpoint is still registered).
0.7.1 traces confirmed the 334 conflict is server-side: a fresh daemon with a different PID + fresh ngrok SDK instance still gets rejected on bind, which means the reservation is held by ngrok's server regardless of what we do locally. The two fixes available to end users are wait out the grace period or stop the endpoint manually — these buttons shortcut both paths.
packages/mcp-server+packages/extensionbump to0.7.2.
- ERR_NGROK_334 ("endpoint already online") now recovers cleanly. The ngrok provider now calls
ngrok.kill()immediately beforengrok.forward()so any in-process listener from a prior enable cycle is torn down before the new one registers. The remaining ~60s grace period is server-side ngrok state we can't short-circuit, but errors now carry a specific actionable message ("Wait ~60 seconds for ngrok's server to release it, then click Enable again. Or: use the Kill daemon button…") instead of the raw upstream error. - Friendly error translations for the three most common ngrok failure codes:
ERR_NGROK_334— domain conflict (see above).ERR_NGROK_105— invalid authtoken.ERR_NGROK_108— free-tier one-session cap violated by another device.
- "Kill daemon" button (
daemon:kill) next to Restart. Confirmation modal; on approval the extension runsstopDaemon({ force: true })which:- Attempts graceful
POST /daemon/shutdown. - If the daemon doesn't respond within 3s, signals the lockfile's pid with
SIGTERMthenSIGKILL. - Releases the lockfile so the next Enable spawns a fresh daemon.
- Does NOT auto-respawn; the user explicitly controls via Restart afterwards.
- Attempts graceful
- Auto-prompt to re-enable after ngrok setting changes. When the user updates the ngrok authtoken or reserved domain while a tunnel is live, a VS Code info-toast offers to disable + re-enable the tunnel so the new settings apply immediately. Pick "Later" to defer.
packages/mcp-server+packages/extensionbump to0.7.1.stopDaemonsignature gains an optionalforce: booleanflag; return type addsforced: boolean. Existing callers (restartDaemon) keep working.
- We can't auto-populate the ngrok reserved-domain dropdown from the account — ngrok's agent authtoken and their REST API key are separate credentials. Adding domain-listing would require a second UI field for the API key; deferred until we see whether users actually want that (vs. typing the domain).
- Pluggable tunnel-provider registry (
packages/mcp-server/src/daemon/tunnel-providers/):cf-quick— existing Cloudflare Quick Tunnel, default, ephemeral*.trycloudflare.comURL.ngrok— persistent URL via the official@ngrok/ngrokNAPI binding (no binary download, no child-process management). Free-tier accounts get one reserved*.ngrok-free.appdomain that persists across daemon restarts.
- Dashboard provider picker — new dropdown in the daemon card swaps between providers. When ngrok is selected but unconfigured, an inline setup widget prompts for the authtoken with a direct link to the ngrok dashboard; optional reserved-domain input afterwards.
- CLI commands for the same flows:
npx perplexity-user-mcp daemon list-providers [--json]npx perplexity-user-mcp daemon set-provider cf-quick|ngroknpx perplexity-user-mcp daemon set-ngrok-authtoken <token>npx perplexity-user-mcp daemon set-ngrok-domain <domain>npx perplexity-user-mcp daemon clear-ngrok
- Persistence: provider choice in
<configDir>/tunnel-settings.json; ngrok credentials in<configDir>/ngrok.json(0600 POSIX / icacls ACL on Windows, mirroring daemon.token).
helmetmiddleware on every HTTP request — setsX-Content-Type-Options,X-Frame-Options: DENY,X-Download-Options,Referrer-Policy, etc. HSTS deliberately off (our origin is HTTP; the tunnel edge supplies TLS).trust proxy=1set on the express app — resolves theValidationError: X-Forwarded-Forwarnings the daemon emitted on every tunnel request and letsexpress-rate-limitcorrectly identify source IPs.- Per-IP rate limit on the OAuth endpoints (
/authorize,/register,/token,/revoke). Tunnel traffic only; 30 req/min per source IP. Prevents bulk dynamic-client-registration abuse on a leaked tunnel URL.
packages/mcp-server+packages/extensionbump to0.7.0.- New deps:
@ngrok/ngrok^1.7.0,helmet^8.1.0. Both added toprepare-package-deps.mjsrootPackages so they ship in the VSIX'sdist/node_modules/. - New subpath export:
perplexity-user-mcp/daemon/tunnel-providers.
- Claude Desktop OAuth flow now completes. 0.6.0's
/mcpbearer middleware did not emit aresource_metadataparameter in theWWW-Authenticate401 header (the SDK'srequireBearerAuthcapturesresourceMetadataUrlat construction time, but our PRM URL is tunnel-host-dependent). Claude Desktop couldn't discover PRM and fell back to POSTing at the bare tunnel URL, which 404'd, producingAuthorization with the MCP server failedafter a successful consent. Replaced with a custom bearer wrapper that readsreq.headers.hoston each 401 and emitsresource_metadata="https://<tunnel>/.well-known/oauth-protected-resource"dynamically. - Bare-URL forgiveness. The
/mcpMCP handler is now mounted at/as well. Users who paste the tunnel URL into their client config without/mcpsuffix still work. A sniffer on the root route forwardsPOST /+ JSON/SSEAcceptto the MCP handler and keeps the branded homepage for browserGET /. - Audit log paths were wrong. Every request going through
mcpAuthRouter's sub-routers was logged asPOST /orGET /becausereq.pathis mount-relative. Switched toreq.originalUrlso/register,/token,/authorize,/revoke,/.well-known/*appear correctly inaudit.log.
packages/mcp-serverandpackages/extensionbump to0.6.1.
- OAuth 2.1 authorization server, implementing the MCP
OAuthServerProviderinterface via a newPerplexityOAuthProvider(inpackages/mcp-server/src/daemon/oauth-provider.ts). Exposes the full RFC 6749 / PKCE-required flow:GET /.well-known/oauth-authorization-server— RFC 8414 authorization server metadata. Dynamic issuer; tunnel clients see the trycloudflare URL, loopback callers see127.0.0.1.GET /.well-known/oauth-protected-resource— RFC 9728 protected-resource metadata pointing at the same issuer.POST /register— RFC 7591 dynamic client registration (public clients; no client_secret).GET /authorize— PKCES256required. Bridges to a VS Code modal via the SSE consent coordinator.POST /token—authorization_code+refresh_tokengrants. Access tokens are opaque (32-byte base64url) with a 1h TTL; refresh tokens rotate on each exchange.POST /revoke— invalidates a given access or refresh token.
- VS Code consent modal. When a client hits
/authorize, the daemon emits adaemon:oauth-consent-requestSSE event. The extension host shows a native modal with client name, client_id, and redirect_uri. Approval/denial routes back through the new/daemon/oauth-consentadmin endpoint (static-bearer gated, so OAuth clients cannot approve their own consent). - Clients persistence at
<configDir>/oauth-clients.json(0600). Access + refresh tokens are kept in memory. /mcpaccepts both auth shapes — the static daemon bearer (for loopback + CLI) and OAuth access tokens (for remote MCP clients like Claude Desktop's custom connector). The SDKrequireBearerAuthmiddleware is used with our provider as the verifier. A smallpromoteCallerClientIdshim promotes ax-perplexity-client-idheader ontoreq.auth.clientIdwhen the caller authenticated via static bearer, so audit and progress-event filters stay meaningful.
packages/mcp-serverandpackages/extensionbump to0.6.0.StartedDaemonServergainslistOAuthClients,revokeOAuthClient, andresolveOAuthConsenthelpers.StartDaemonServerOptionsgainsonOAuthConsentRequestandgetTunnelUrlhooks.
- Consent modal is the only path that issues an authorization code — browser-only flows (just hitting
/authorize) can't self-approve. - Consent times out after 2 minutes with implicit deny. Each consent requires re-approval — we do not cache approvals across
/authorizecalls. - Static-bearer callers are reported as
clientId: "local-static"inverifyAccessTokenunless they passx-perplexity-client-id.
- Branded unauthenticated homepage at
GET /,robots.txtwithDisallow: /, and a favicon 204. Hitting the tunnel URL in a browser now shows a clear "not a public service" card instead of leakingCannot GET /. - Security middleware (
packages/mcp-server/src/daemon/security.ts) running before bearer auth on every request:- Per-bearer rate limit on tunnel traffic (default 60 req/min, override with
PERPLEXITY_DAEMON_RATELIMIT_RPM). Loopback traffic is exempt. - Suspicious-User-Agent blocklist (
masscan,nmap,zgrab,sqlmap,nikto,gobuster,wpscan,hydra,Shodan,censys). - Slow-401 — every tunnel 401 is delayed 150ms to defeat bearer brute-force timing probes.
- Per-bearer rate limit on tunnel traffic (default 60 req/min, override with
- 401-burst auto-disable tripwire — 20 auth failures within 60s on the tunnel snip the tunnel immediately. The dashboard raises an error banner with recovery guidance (rotate bearer → re-enable).
- Enriched audit log — every HTTP request to
/daemon/*,/mcp,/authorize,/token,/registernow appends a JSONL entry withip,userAgent,path,httpStatus,auth, andsourcein addition to the existing tool-call fields.
packages/mcp-serverandpackages/extensionbump to0.5.1.appendAuditEntrysignature extended with optionalip,userAgent,path,httpStatus,authfields (backward compatible — tool-call audit rows still work unchanged).
- Tunnel auto-disable is source-scoped:
x-perplexity-source: loopbackand true 127.0.0.1 traffic withoutcf-connecting-ipnever triggers the tripwire. - Homepage + robots deliberately leak no runtime information (no version, uptime, tool list, or port). The dashboard remains the only authoritative source of that data.
- Markdown-backed history storage under per-profile
history/*.mdwith YAML frontmatter, sidecar attachments, and rebuildableindex.json. - Native export support for PDF / Markdown / DOCX through the captured Perplexity
/rest/entry/exportflow, exposed via the MCP tool, CLI, and VS Code dashboard. - External Markdown viewer registry and detection for Obsidian, Typora, and Logseq, including an Obsidian bridge copy path and doctor visibility through
ide.mdViewers. - VS Code Rich View overlay, History tab actions, export/download flows, preview/open-with actions, and command-palette entries for
Open Rich View,Export History Entry, andRebuild History Index. - Operator docs: docs/export-endpoint-capture.md and docs/history-migration.md.
perplexity_list_researchesandperplexity_get_researchnow read from the unified Markdown history store instead of a separate JSON research store.packages/mcp-serverandpackages/extensionnow ship as0.5.0.- Extension bundling now keeps
keytarexternal again so VSIX/extension builds do not try to inlinekeytar.node.
- Pre-0.5.0 flat
history.jsonandresearches/*.jsonfiles are not auto-converted. New entries populate the Markdown layout only. See docs/history-migration.md.
- Doctor
ide-auditwas alwaysskipin exported reports. Thedoctor:exportanddoctor:report-issuehandlers calledrunDoctor({ baseDir })without passingideStatuses, so the IDE check always fell through to its "requires the VS Code extension" skip branch. Both handlers now pass the sameideStatusesthe Run/Deep-check path uses.
- One-click fix actions for known-remediable doctor findings.
DoctorChecknow carries an optionalaction: { label, commandId, args? }that the webview renders as a button next to the hint. Extension host whitelists the allowedcommandIds and clears the cached report after running so the next Run shows the now-fixed state. - Action producers wired for three findings:
config/active-pointer: warn(no active profile) → Add account (Perplexity.addAccount).native-deps/impit: skip→ Install Speed Boost (Perplexity.installSpeedBoost).ide/<name>: warn(detected but not configured or stale) → Configure (Perplexity.generateConfigs,args: [id]).
- Create-account flow: adding a profile from the dashboard or extension host now creates it, makes it active immediately, and starts the selected login mode in the same flow instead of forcing a second separate login action.
- Generic login targeting: the shared
Perplexity.loginpath now uses the active profile's savedloginModeinstead of prompting again and risking a fallback to the olddefaultprofile. - Empty-profile UX: the webview now shows
No Account Yet/Add accountwhen no active profile exists, and the profile switcher no longer pretends the active profile isdefaultafter all profiles are deleted. - Mode-aware re-login: dashboard re-login actions now route through the generic profile-aware login path instead of hard-coding manual mode.
- Manual login visibility: the headed manual login runner no longer starts Chrome minimized. It now brings the browser tab to the front and the extension shows an explicit prompt telling the user to complete sign-in there.
- Delete profile action: the dashboard no longer relies on
window.confirm(...)inside the webview. Confirmation now runs on the extension host via a modal VS Code warning, so the delete action reaches the real profile-removal path reliably. - Regression coverage: extension auth tests now cover the
awaiting_userprogress event emitted by the manual login path.
- Doctor speed-boost detection: the
native-depscheck now detectsimpitfrom the actual runtime install under~/.perplexity-mcp/native-deps/node_modules/impit, instead of relying on an import path that could miss a valid install and incorrectly reportnot installed. - Profile deletion semantics: deleting a profile now clears or re-points the active profile pointer instead of leaving stale state behind. The dashboard now exposes an explicit
Delete profile…action for full profile removal. - Headed login window behavior: manual and auto login runners now attempt to start minimized and use a CDP minimize call as a best-effort fallback so the browser is less intrusive on the desktop.
- Doctor probe false-fail: when the live probe completes on an authenticated session but Perplexity returns zero citations, doctor now reports a warning instead of a hard auth failure.
- Real-site auto OTP flow: the auto login runner now drives Perplexity's live NextAuth email+OTP flow (
/api/auth/csrf,/api/auth/signin/email,/auth/verify-request,/api/auth/otp-redirect-link,/api/auth/callback/email) instead of treating the site as unsupported because/login/emailis absent. - Post-login account metadata: the auto runner, manual runner, and health check now collect session, model, rate-limit, ASI, experiment, and user-info data from the current live endpoints so profile caches and doctor output reflect the authenticated account correctly.
- Release packaging clarity: the fixed auth build now ships as
0.4.3, avoiding stale0.4.2installs that still bundle the older mock-only login runner and outdated dashboard fallback copy.
- Active profile drift: dashboard snapshots, live model refresh, and the shared MCP client now resolve profile-specific paths at call time instead of caching
defaultat module import. Profile switches and per-profile logins now read/write the selected profile consistently. - Webview auth/profile actions now refresh MCP definitions: the dashboard path (
auth:login-start,auth:logout,profile:switch) now triggers the same MCP server definition refresh that the command-palette path already did, so switching or logging into a non-default profile updates the running server instead of leaving it on the old account. - Doctor runtime packaged-path crash: the runtime check now resolves
package.jsonfrom the extension-providedbaseDirbefore falling back toimport.meta.url, which fixes theruntime-runner -- check crashed: Invalid URLfailure in packaged VSIX builds. - VSIX build order:
packages/extensionnow rebuilds@perplexity-user-mcp/sharedandperplexity-user-mcpbefore bundlingextension.js, preventing stale workspace dist output from being shipped inside a new VSIX.
- Login:
AuthManagernow derives runner paths fromvscode.ExtensionContext.extensionUriinstead ofglobalThis.require.resolve(...). The latter doesn't exist in the tsup-bundled CJS extension, so 0.4.0's Login button always threw"require not available in this runtime". Phase 2 regression — not caught because 0.3.0 shipped without a manual VSIX smoke. - Doctor false-positive on
native-deps: thepatchrightandgot-scraping-chainchecks now accept abaseDiropt.DashboardProviderpasses<extensionUri>/distso the chain resolves against the VSIX'sdist/node_modules/tree. PreviouslycreateRequire(import.meta.url)failed because tsup polyfillsimport_meta = {}in CJS bundles. - Redactor no longer eats ISO timestamps: the IPv6 regex used to match any colon-separated hex-chars-and-digits, which included wall-clock
HH:MM:SSstrings. Now requires IPv6-shape (hex groups AND either a double-colon or a group with hex-only chars). Doctor reports now showGenerated: 2026-04-20T10:27:42.278Zverbatim. - Doctor tab moved from position 2 to position 5 — it's not a daily-driver tab.
- Added Phase 3.1 manual smoke checklist in
docs/smoke-tests.md. Every future phase's release gate now requires a successful VSIX install + smoke run before tagging.
perplexity-user-mcp doctorCLI subcommand with 10 check categories (runtime, config, profiles, vault, browser, native-deps, network, ide, mcp, probe).--probeopt-in live search check,--jsonmachine-readable output,--allmulti-profile mode,--profilesingle-profile targeting.perplexity_doctor({probe?, profile?})MCP tool — same checks, Markdown-rendered output for LLMs.- VS Code extension Doctor dashboard tab with collapsible category cards, inline action buttons, and Run / Deep check / Export / Report-issue toolbar.
- Guided GitHub issue flow with client-side redaction (emails, userIds, cookies, home paths, IPs, long tokens) and opt-out via
reporting.githubIssueButton: falsein~/.perplexity-mcp/config.json. .github/ISSUE_TEMPLATE/doctor-report.ymlstructured form with consent checkboxes.- Regression guard for Phase 2 carry-over #5: the
native-deps/got-scraping-chaincheck walksheader-generator → dot-prop → is-objviacreateRequireand warns if the VSIX packaging chain breaks. - New extension commands
Perplexity.doctorandPerplexity.doctorReportIssue. - Integration tests covering doctor end-to-end + probe timeout + packaging-chain regression.
tools-config.jsonread-onlyprofile now includesperplexity_doctor.McpServerversion string advertised as0.4.0.packages/extension/scripts/prepare-package-deps.mjsnow has a JSDoc header documenting whydot-propandis-objare inrootPackages.
packages/mcp-server/src/health-check.js— spawnable non-persistent session probepackages/mcp-server/src/manual-login-runner.js— spawnable headed-browser loginpackages/mcp-server/src/login-runner.js— spawnable auto-OTP login with IPC prompt + retrypackages/mcp-server/src/logout.js— soft + hard (--purge) logoutpackages/mcp-server/src/reinit-watcher.js—.reinitsentinel watcher with debouncepackages/mcp-server/src/tty-prompt.js— vault passphrase prompt (priority-3 fallback)- Express-based mock Perplexity server for integration tests
(
packages/mcp-server/test/integration/mock-server.js) - Integration test coverage for all four runners + end-to-end re-auth cycle
- Shared types:
AuthStatus,AuthState,Profile+ 10 new message variants (3ExtensionMessage, 7WebviewMessage) - Webview components:
ProfileSwitcher,OtpModal,ExpiredBanner+ auth slice on the zustand store - Extension commands:
Perplexity.logout,Perplexity.switchAccount,Perplexity.addAccount(+Perplexity.loginnow routed through the newAuthManagerwith per-profile concurrency guards and OTP IPC) - Manual smoke checklist at docs/smoke-tests.md
packages/mcp-server/src/index.ts: removed theclientReady=trueone-shot trap;getClient()now always awaits the latest init promise, and.reinitsentinel triggersclient.reinit()so external runners take effect without a server restart (fixes TODO #2, #6).packages/mcp-server/src/client.ts:getSavedCookies()is vault-backed and async;loginViaBrowserremoved (moved to runners).packages/mcp-server/src/config.ts: cookie / browser-data paths resolve through the active profile; new asynchasStoredLogin().packages/mcp-server/src/cli.js:login/logout/status/add-account/switch-account/list-accountsare real implementations (Phase-1 stubs replaced).packages/extension/src/mcp/auth-manager.ts: full implementation (login, logout, checkSession, concurrency guard, OTP IPC, state machine).packages/extension/src/mcp/secure-permissions.ts: Windows user resolver falls back toUSERPROFILEbasename, thenwhoami.packages/extension/scripts/prepare-package-deps.mjs: VSIX now shipsdot-prop+is-objso the got-scraping tier works post-install.packages/mcp-server/package.json: added./logoutand./profilessubpath exports for the extension's dynamic imports.
- TODO #1 — encrypted multi-account login (Phase 1 scaffolding + Phase 2 runtime).
- TODO #2 —
perplexity_loginMCP tool now works end-to-end via runner + sentinel re-init. - TODO #5 — logout flow exposed via CLI + dashboard + command palette.
- TODO #6 — MCP server no longer caches
clientReady=truethrough a login. - Phase 2 carry-overs #1–#5 from Phase 1 final review: key-cache reset
on
setActive, vault JSON corruption errors surfaced viaredact,secureWindowsuser-resolver fallbacks, IPC discipline verified across all runners (single stdout write, progress viaprocess.send), VSIX dot-prop chain included.
- Runners never write cookies or user IDs to disk in plaintext; vault is AES-256-GCM with a 256-bit master key in the OS keychain (or env-var passphrase / TTY prompt fallback).
- Extension ↔ webview: user IDs and emails are NOT forwarded to the
webview (only
tier+status). - Corrupt vault detection now surfaces a diagnosable error (redacted) instead of silently returning empty.
- OTP submissions are routed per-profile so concurrent logins across different profiles don't cross-deliver codes.
- No automatic migration from 0.2.0 or earlier flat
~/.perplexity-mcp/cookies.json. Users must re-login once with 0.3.0 to populate the per-profile vault. Documented in docs/superpowers/specs/2026-04-19-perplexity-user-mcp-upgrade-design.md §15.
- 159/159 automated tests pass (128 unit + 31 integration across 12 test
files on
perplexity-user-mcp; 13 onperplexity-vscode). - All 4 package typechecks clean.
- Manual smoke checklist (docs/smoke-tests.md) pending verification on macOS 14+ and Ubuntu 22+; Windows 11 partial (automated integration tests exercise the runner + mock flow).
- LICENSE (MIT), NOTICE, SECURITY.md, CHANGELOG.md
packages/mcp-server/src/redact.js— security-critical log redactionpackages/mcp-server/src/profiles.js— multi-account profile CRUDpackages/mcp-server/src/vault.js— disk-backed AES-256-GCM vault with OS-keychain-first master key acquisition and documented fallbackspackages/mcp-server/src/cli.js— subcommand dispatcher (stubs in this phase; real behavior arrives in Phases 2-4)packages/extension/src/mcp/secure-permissions.ts— filesystem hardeningpackages/extension/src/mcp/auth-manager.ts— fork harness skeletonkeytaras optional runtime dependency
- Initial release as
perplexity-user-mcp. - License: UNLICENSED → MIT
packages/mcp-server/package.jsonbinnow points atcli.mjs
- Any existing MCP tool behavior
- Any existing dashboard feature
- Any existing IDE auto-config flow