@@ -33,6 +33,7 @@ import { isImpitAvailable, impitFetchJson } from "./refresh.js";
3333import { writeFileSync , readFileSync , mkdirSync , existsSync } from "fs" ;
3434import { join } from "path" ;
3535import { getActiveName , getConfigDir , getProfilePaths } from "./profiles.js" ;
36+ import type { DaemonAuthStatus } from "@perplexity-user-mcp/shared" ;
3637import { clearStaleSingletonLocks } from "./fs-utils.js" ;
3738
3839function getActiveProfileName ( ) : string {
@@ -894,69 +895,77 @@ export class PerplexityClient {
894895 * Set env PERPLEXITY_HEADLESS_ONLY=1 to skip the headed phase (uses disk cache).
895896 */
896897 async init ( ) : Promise < void > {
897- const activePaths = getActivePaths ( ) ;
898- if ( ! existsSync ( activePaths . browserData ) ) {
899- mkdirSync ( activePaths . browserData , { recursive : true } ) ;
900- }
898+ const _initAt = Date . now ( ) ;
899+ try {
900+ const activePaths = getActivePaths ( ) ;
901+ if ( ! existsSync ( activePaths . browserData ) ) {
902+ mkdirSync ( activePaths . browserData , { recursive : true } ) ;
903+ }
901904
902- // Fail fast with a readable message if no browser is installed at all.
903- const browser = await resolveBrowserExecutable ( ) ;
904- console . error ( `[perplexity-mcp] Using ${ browser . source } : ${ browser . path } ` ) ;
905+ // Fail fast with a readable message if no browser is installed at all.
906+ const browser = await resolveBrowserExecutable ( ) ;
907+ console . error ( `[perplexity-mcp] Using ${ browser . source } : ${ browser . path } ` ) ;
905908
906- // Phase 1: Headed session — solve CF challenge + fetch account info
907- const skipHeaded = process . env . PERPLEXITY_HEADLESS_ONLY === "1" ;
908- if ( ! skipHeaded ) {
909- await this . headedBootstrap ( ) ;
910- } else {
911- console . error ( "[perplexity-mcp] Skipping headed session (PERPLEXITY_HEADLESS_ONLY=1)." ) ;
912- this . loadCachedAccountInfo ( ) ;
913- }
914-
915- // Phase 2: Headless browser for search operations.
916- // Use the SAME persistent browserData directory as Phase 1 so that
917- // any cf_clearance cookie acquired during the headed bootstrap is
918- // already on disk and loaded automatically. This fixes the bug where
919- // Phase 2 used a non-persistent context and only had stale vault
920- // cookies (issue #5).
921- console . error ( "[perplexity-mcp] Launching headless persistent browser..." ) ;
922- const launchOpts = buildLaunchOptions ( true ) ;
923- this . context = await chromium . launchPersistentContext (
924- activePaths . browserData ,
925- launchOpts ,
926- ) ;
927- this . browser = this . context . browser ( ) ;
928-
929- // Inject vault cookies only for cookies not already present on disk.
930- // The headed bootstrap may have refreshed cf_clearance; we must not
931- // overwrite the fresh disk cookie with the stale vault copy.
932- const saved = await getSavedCookies ( ) ;
933- if ( saved . length > 0 ) {
934- const current = await this . context . cookies ( ) ;
935- const currentNames = new Set ( current . map ( ( c ) => c . name ) ) ;
936- const toInject = saved . filter ( ( c ) => ! currentNames . has ( c . name ) ) ;
937- if ( toInject . length > 0 ) {
938- await this . context . addCookies ( toInject ) ;
939- console . error ( `[perplexity-mcp] Injected ${ toInject . length } missing cookies from vault.` ) ;
909+ // Phase 1: Headed session — solve CF challenge + fetch account info
910+ const skipHeaded = process . env . PERPLEXITY_HEADLESS_ONLY === "1" ;
911+ if ( ! skipHeaded ) {
912+ await this . headedBootstrap ( ) ;
940913 } else {
941- console . error ( "[perplexity-mcp] All vault cookies already present on disk; skipping injection." ) ;
914+ console . error ( "[perplexity-mcp] Skipping headed session (PERPLEXITY_HEADLESS_ONLY=1)." ) ;
915+ this . loadCachedAccountInfo ( ) ;
942916 }
943- }
944917
945- this . page = await this . context . newPage ( ) ;
918+ // Phase 2: Headless browser for search operations.
919+ // Use the SAME persistent browserData directory as Phase 1 so that
920+ // any cf_clearance cookie acquired during the headed bootstrap is
921+ // already on disk and loaded automatically. This fixes the bug where
922+ // Phase 2 used a non-persistent context and only had stale vault
923+ // cookies (issue #5).
924+ console . error ( "[perplexity-mcp] Launching headless persistent browser..." ) ;
925+ const launchOpts = buildLaunchOptions ( true ) ;
926+ this . context = await chromium . launchPersistentContext (
927+ activePaths . browserData ,
928+ launchOpts ,
929+ ) ;
930+ this . browser = this . context . browser ( ) ;
931+
932+ // Inject vault cookies only for cookies not already present on disk.
933+ // The headed bootstrap may have refreshed cf_clearance; we must not
934+ // overwrite the fresh disk cookie with the stale vault copy.
935+ const saved = await getSavedCookies ( ) ;
936+ if ( saved . length > 0 ) {
937+ const current = await this . context . cookies ( ) ;
938+ const currentNames = new Set ( current . map ( ( c ) => c . name ) ) ;
939+ const toInject = saved . filter ( ( c ) => ! currentNames . has ( c . name ) ) ;
940+ if ( toInject . length > 0 ) {
941+ await this . context . addCookies ( toInject ) ;
942+ console . error ( `[perplexity-mcp] Injected ${ toInject . length } missing cookies from vault.` ) ;
943+ } else {
944+ console . error ( "[perplexity-mcp] All vault cookies already present on disk; skipping injection." ) ;
945+ }
946+ }
946947
947- // Navigate to Perplexity (headless — relies on fresh cf_clearance from headed phase)
948- try {
949- await this . page . goto ( PERPLEXITY_URL , { waitUntil : "domcontentloaded" , timeout : 30000 } ) ;
950- await this . page . waitForTimeout ( 2000 ) ;
951- } catch ( err ) {
952- console . error ( "[perplexity-mcp] Navigation warning:" , ( err as Error ) . message ) ;
953- }
948+ this . page = await this . context . newPage ( ) ;
954949
955- await this . checkAuth ( ) ;
950+ // Navigate to Perplexity (headless — relies on fresh cf_clearance from headed phase)
951+ try {
952+ await this . page . goto ( PERPLEXITY_URL , { waitUntil : "domcontentloaded" , timeout : 30000 } ) ;
953+ await this . page . waitForTimeout ( 2000 ) ;
954+ } catch ( err ) {
955+ console . error ( "[perplexity-mcp] Navigation warning:" , ( err as Error ) . message ) ;
956+ }
957+
958+ await this . checkAuth ( ) ;
956959
957- // If headed phase was skipped or failed, try loading account info from headless
958- if ( ! this . accountInfo . modelsConfig ) {
959- await this . loadAccountInfo ( ) ;
960+ // If headed phase was skipped or failed, try loading account info from headless
961+ if ( ! this . accountInfo . modelsConfig ) {
962+ await this . loadAccountInfo ( ) ;
963+ }
964+
965+ this . writeDaemonStatus ( _initAt , null ) ;
966+ } catch ( err : unknown ) {
967+ this . writeDaemonStatus ( _initAt , err instanceof Error ? err . message : String ( err ) ) ;
968+ throw err ;
960969 }
961970 }
962971
@@ -2601,5 +2610,42 @@ export class PerplexityClient {
26012610 await this . browser . close ( ) . catch ( ( ) => { } ) ;
26022611 this . browser = null ;
26032612 }
2613+ this . authenticated = false ;
2614+ this . userId = null ;
2615+ this . writeDaemonStatus ( Date . now ( ) , null ) ;
2616+ }
2617+
2618+ // ── Daemon status file ─────────────────────────────────────────────────────
2619+
2620+ private daemonTier ( ) : DaemonAuthStatus [ "tier" ] {
2621+ if ( ! this . authenticated ) return "Anonymous" ;
2622+ if ( this . accountInfo . isMax ) return "Max" ;
2623+ if ( this . accountInfo . isPro ) return "Pro" ;
2624+ if ( this . accountInfo . isEnterprise ) return "Enterprise" ;
2625+ return "Authenticated" ;
2626+ }
2627+
2628+ /**
2629+ * Write daemon-status.json so the extension UI can show live auth state
2630+ * instead of relying on the stale models-cache.json snapshot.
2631+ * @param startedAt - Date.now() captured at the start of init/reinit
2632+ * @param error - error message if init threw, null on success or shutdown
2633+ */
2634+ private writeDaemonStatus ( startedAt : number , error : string | null ) : void {
2635+ try {
2636+ const paths = getActivePaths ( ) ;
2637+ const status : DaemonAuthStatus = {
2638+ authenticated : this . authenticated ,
2639+ tier : this . daemonTier ( ) ,
2640+ userId : this . userId ,
2641+ pid : process . pid ,
2642+ lastInit : new Date ( ) . toISOString ( ) ,
2643+ initDurationMs : Date . now ( ) - startedAt ,
2644+ error,
2645+ } ;
2646+ writeFileSync ( paths . daemonStatus , JSON . stringify ( status , null , 2 ) + "\n" ) ;
2647+ } catch {
2648+ // Never let a status write crash the daemon.
2649+ }
26042650 }
26052651}
0 commit comments