diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index 2b34d366..d8669c33 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -179,13 +179,17 @@ export class CDPBridge { } class CDPPage implements IPage { + private _pageEnabled = false; constructor(private bridge: CDPBridge) {} /** Navigate with proper load event waiting (P1 fix #3) */ async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise { - await this.bridge.send('Page.enable'); + if (!this._pageEnabled) { + await this.bridge.send('Page.enable'); + this._pageEnabled = true; + } const loadPromise = this.bridge.waitForEvent('Page.loadEventFired', 30_000) - .catch(() => {}); // Don't fail if event times out + .catch(() => {}); // Don't fail if load event times out — page may be an SPA await this.bridge.send('Page.navigate', { url }); await loadPromise; // Smart settle: use DOM stability detection instead of fixed sleep. diff --git a/src/browser/mcp.ts b/src/browser/mcp.ts index c5e80634..4349b644 100644 --- a/src/browser/mcp.ts +++ b/src/browser/mcp.ts @@ -9,6 +9,7 @@ import * as fs from 'node:fs'; import type { IPage } from '../types.js'; import { Page } from './page.js'; import { isDaemonRunning, isExtensionConnected } from './daemon-client.js'; +import { DEFAULT_DAEMON_PORT } from '../constants.js'; const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension @@ -112,7 +113,7 @@ export class BrowserBridge { throw new Error( 'Failed to start opencli daemon. Try running manually:\n' + ` node ${daemonPath}\n` + - 'Make sure port 19825 is available.', + `Make sure port ${DEFAULT_DAEMON_PORT} is available.`, ); } } diff --git a/src/browser/page.ts b/src/browser/page.ts index 0251b226..5576fe01 100644 --- a/src/browser/page.ts +++ b/src/browser/page.ts @@ -184,7 +184,7 @@ export class Page implements IPage { await new Promise(resolve => setTimeout(resolve, options * 1000)); return; } - if (options.time) { + if (typeof options.time === 'number') { await new Promise(resolve => setTimeout(resolve, options.time! * 1000)); return; } diff --git a/src/build-manifest.ts b/src/build-manifest.ts index 633ea905..bf853f6e 100644 --- a/src/build-manifest.ts +++ b/src/build-manifest.ts @@ -179,7 +179,8 @@ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] { choices: parseInlineChoices(body), }); - cursor = objectStart + body.length + 2; + cursor = objectStart + body.length; + if (cursor <= objectStart) break; // safety: prevent infinite loop } return args; @@ -300,7 +301,7 @@ export function scanTs(filePath: string, site: string): ManifestEntry | null { * prefer the TS version (it self-registers and typically has richer logic). */ export function shouldReplaceManifestEntry(current: ManifestEntry, next: ManifestEntry): boolean { - if (current.type === next.type) return true; + if (current.type === next.type) return false; return current.type === 'yaml' && next.type === 'ts'; } diff --git a/src/cascade.ts b/src/cascade.ts index fc4c8120..3e8662aa 100644 --- a/src/cascade.ts +++ b/src/cascade.ts @@ -145,9 +145,10 @@ export async function cascadeProbe( url: string, opts: { maxStrategy?: Strategy; timeout?: number } = {}, ): Promise { - const maxIdx = opts.maxStrategy + const rawIdx = opts.maxStrategy ? CASCADE_ORDER.indexOf(opts.maxStrategy) : CASCADE_ORDER.indexOf(Strategy.HEADER); // Don't auto-try INTERCEPT/UI + const maxIdx = rawIdx === -1 ? CASCADE_ORDER.indexOf(Strategy.HEADER) : rawIdx; const probes: ProbeResult[] = []; diff --git a/src/download/index.ts b/src/download/index.ts index 21e4d166..d8e2963d 100644 --- a/src/download/index.ts +++ b/src/download/index.ts @@ -199,7 +199,9 @@ export function exportCookiesToNetscape( const cookiePath = cookie.path || '/'; const secure = cookie.secure ? 'TRUE' : 'FALSE'; const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now - lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${cookie.name}\t${cookie.value}`); + const safeName = cookie.name.replace(/[\t\n\r]/g, ''); + const safeValue = cookie.value.replace(/[\t\n\r]/g, ''); + lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${safeName}\t${safeValue}`); } fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -237,8 +239,13 @@ export async function ytdlpDownload( '--progress', ]; - if (cookiesFile && fs.existsSync(cookiesFile)) { - args.push('--cookies', cookiesFile); + if (cookiesFile) { + if (fs.existsSync(cookiesFile)) { + args.push('--cookies', cookiesFile); + } else { + console.error(`[download] Cookies file not found: ${cookiesFile}, falling back to browser cookies`); + args.push('--cookies-from-browser', 'chrome'); + } } else { // Try to use browser cookies args.push('--cookies-from-browser', 'chrome'); diff --git a/src/execution.ts b/src/execution.ts index 4aa5a04b..b9841851 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -157,7 +157,9 @@ export async function executeCommand( // Each adapter controls this via `navigateBefore` (see CliCommand docs). const preNavUrl = resolvePreNav(cmd); if (preNavUrl) { - try { await page.goto(preNavUrl); await page.wait(2); } catch {} + try { await page.goto(preNavUrl); await page.wait(2); } catch (err) { + if (debug) console.error(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`); + } } return runWithTimeout(runCommand(cmd, page, kwargs, debug), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, diff --git a/src/plugin.ts b/src/plugin.ts index e200dbfc..2dd2399c 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -32,8 +32,8 @@ function postInstallLifecycle(pluginDir: string): void { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }); - } catch { - // Non-fatal: npm install may fail if no real deps + } catch (err) { + console.error(`[plugin] npm install failed in ${pluginDir}: ${err instanceof Error ? err.message : err}`); } // Symlink host opencli so TS plugins resolve '@jackwener/opencli/registry' diff --git a/src/runtime.ts b/src/runtime.ts index f2ebd343..a88a5f08 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -10,10 +10,21 @@ export function getBrowserFactory(): new () => IBrowserFactory { return (process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge) as unknown as new () => IBrowserFactory; } -export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10); -export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10); -export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10); -export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10); +function parseEnvTimeout(envVar: string, fallback: number): number { + const raw = process.env[envVar]; + if (raw === undefined) return fallback; + const parsed = parseInt(raw, 10); + if (Number.isNaN(parsed) || parsed <= 0) { + console.error(`[runtime] Invalid ${envVar}="${raw}", using default ${fallback}s`); + return fallback; + } + return parsed; +} + +export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30); +export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60); +export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120); +export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_SMOKE_TIMEOUT', 60); /** * Timeout with seconds unit. Used for high-level command timeouts.