Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/browser/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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.
Expand Down
3 changes: 2 additions & 1 deletion src/browser/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.`,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions src/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
}

Expand Down
3 changes: 2 additions & 1 deletion src/cascade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ export async function cascadeProbe(
url: string,
opts: { maxStrategy?: Strategy; timeout?: number } = {},
): Promise<CascadeResult> {
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[] = [];

Expand Down
13 changes: 10 additions & 3 deletions src/download/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 3 additions & 1 deletion src/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
19 changes: 15 additions & 4 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading