Skip to content
Closed
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
10 changes: 2 additions & 8 deletions src/browser/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,7 @@ class CDPPage implements IPage {
});
const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
if (options.path) {
const fs = await import('node:fs');
const path = await import('node:path');
const dir = path.dirname(options.path);
await fs.promises.mkdir(dir, { recursive: true });
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
await saveBase64ToFile(base64, options.path);
}
return base64;
}
Expand Down Expand Up @@ -341,9 +337,7 @@ class CDPPage implements IPage {
}
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
import { isRecord, saveBase64ToFile } from '../utils.js';

function isCookie(value: unknown): value is BrowserCookie {
return isRecord(value)
Expand Down
69 changes: 33 additions & 36 deletions src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { formatSnapshot } from '../snapshotFormatter.js';
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
import { sendCommand } from './daemon-client.js';
import { wrapForEval } from './utils.js';
import { saveBase64ToFile } from '../utils.js';
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
import { generateStealthJs } from './stealth.js';
import {
Expand All @@ -36,20 +37,23 @@ export class Page implements IPage {
/** Active tab ID, set after navigate and used in all subsequent commands */
private _tabId: number | undefined;

/** Helper: spread tabId into command params if we have one */
private _tabOpt(): { tabId: number } | Record<string, never> {
return this._tabId !== undefined ? { tabId: this._tabId } : {};
/** Helper: spread workspace into command params */
private _wsOpt(): { workspace: string } {
return { workspace: this.workspace };
}

private _workspaceOpt(): { workspace: string } {
return { workspace: this.workspace };
/** Helper: spread workspace + tabId into command params */
private _cmdOpts(): Record<string, unknown> {
return {
workspace: this.workspace,
...(this._tabId !== undefined && { tabId: this._tabId }),
};
}

async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void> {
const result = await sendCommand('navigate', {
url,
...this._workspaceOpt(),
...this._tabOpt(),
...this._cmdOpts(),
}) as { tabId?: number };
// Remember the tabId for subsequent exec calls
if (result?.tabId) {
Expand All @@ -59,8 +63,7 @@ export class Page implements IPage {
try {
await sendCommand('exec', {
code: generateStealthJs(),
...this._workspaceOpt(),
...this._tabOpt(),
...this._cmdOpts(),
});
} catch {
// Non-fatal: stealth is best-effort
Expand All @@ -71,28 +74,27 @@ export class Page implements IPage {
const maxMs = options?.settleMs ?? 1000;
await sendCommand('exec', {
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
...this._workspaceOpt(),
...this._tabOpt(),
...this._cmdOpts(),
});
}
}

/** Close the automation window in the extension */
async closeWindow(): Promise<void> {
try {
await sendCommand('close-window', { ...this._workspaceOpt() });
await sendCommand('close-window', { ...this._wsOpt() });
} catch {
// Window may already be closed or daemon may be down
}
}

async evaluate(js: string): Promise<unknown> {
const code = wrapForEval(js);
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
return sendCommand('exec', { code, ...this._cmdOpts() });
}

async getCookies(opts: { domain?: string; url?: string } = {}): Promise<BrowserCookie[]> {
const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
return Array.isArray(result) ? result : [];
}

Expand All @@ -108,7 +110,7 @@ export class Page implements IPage {
});

try {
const result = await sendCommand('exec', { code: snapshotJs, ...this._workspaceOpt(), ...this._tabOpt() });
const result = await sendCommand('exec', { code: snapshotJs, ...this._cmdOpts() });
// The advanced engine already produces a clean, pruned, LLM-friendly output.
// Do NOT pass through formatSnapshot — its format is incompatible.
return result;
Expand Down Expand Up @@ -148,35 +150,35 @@ export class Page implements IPage {
return buildTree(document.body, 0);
})()
`;
const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
const raw = await sendCommand('exec', { code, ...this._cmdOpts() });
if (opts.raw) return raw;
if (typeof raw === 'string') return formatSnapshot(raw, opts);
return raw;
}

async click(ref: string): Promise<void> {
const code = clickJs(ref);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}

async typeText(ref: string, text: string): Promise<void> {
const code = typeTextJs(ref, text);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}

async pressKey(key: string): Promise<void> {
const code = pressKeyJs(key);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}

async scrollTo(ref: string): Promise<unknown> {
const code = scrollToRefJs(ref);
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
return sendCommand('exec', { code, ...this._cmdOpts() });
}

async getFormState(): Promise<Record<string, unknown>> {
const code = getFormStateJs();
return (await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() })) as Record<string, unknown>;
return (await sendCommand('exec', { code, ...this._cmdOpts() })) as Record<string, unknown>;
}

async wait(options: number | WaitOptions): Promise<void> {
Expand All @@ -191,35 +193,35 @@ export class Page implements IPage {
if (options.text) {
const timeout = (options.timeout ?? 30) * 1000;
const code = waitForTextJs(options.text, timeout);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}
}

async tabs(): Promise<unknown[]> {
const result = await sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
const result = await sendCommand('tabs', { op: 'list', ...this._wsOpt() });
return Array.isArray(result) ? result : [];
}

async closeTab(index?: number): Promise<void> {
await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
await sendCommand('tabs', { op: 'close', ...this._wsOpt(), ...(index !== undefined ? { index } : {}) });
// Invalidate cached tabId — the closed tab might have been our active one.
// We can't know for sure (close-by-index doesn't return tabId), so reset.
this._tabId = undefined;
}

async newTab(): Promise<void> {
const result = await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() }) as { tabId?: number };
const result = await sendCommand('tabs', { op: 'new', ...this._wsOpt() }) as { tabId?: number };
if (result?.tabId) this._tabId = result.tabId;
}

async selectTab(index: number): Promise<void> {
const result = await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() }) as { selected?: number };
const result = await sendCommand('tabs', { op: 'select', index, ...this._wsOpt() }) as { selected?: number };
if (result?.selected) this._tabId = result.selected;
}

async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
const code = networkRequestsJs(includeStatic);
const result = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
const result = await sendCommand('exec', { code, ...this._cmdOpts() });
return Array.isArray(result) ? result : [];
}

Expand All @@ -241,34 +243,29 @@ export class Page implements IPage {
*/
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
const base64 = await sendCommand('screenshot', {
...this._workspaceOpt(),
...this._cmdOpts(),
format: options.format,
quality: options.quality,
fullPage: options.fullPage,
...this._tabOpt(),
}) as string;

if (options.path) {
const fs = await import('node:fs');
const path = await import('node:path');
const dir = path.dirname(options.path);
await fs.promises.mkdir(dir, { recursive: true });
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
await saveBase64ToFile(base64, options.path);
}

return base64;
}

async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
const code = scrollJs(direction, amount);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}

async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
const times = options.times ?? 3;
const delayMs = options.delayMs ?? 2000;
const code = autoScrollJs(times, delayMs);
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
await sendCommand('exec', { code, ...this._cmdOpts() });
}

async installInterceptor(pattern: string): Promise<void> {
Expand Down
4 changes: 1 addition & 3 deletions src/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ interface YamlCliDefinition {
navigateBefore?: boolean | string;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
import { isRecord } from './utils.js';


function extractBalancedBlock(
Expand Down
2 changes: 0 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,6 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
const r = await generateCliFromUrl({
url,
BrowserFactory: getBrowserFactory(),
builtinClis: BUILTIN_CLIS,
userClis: USER_CLIS,
goal: opts.goal,
site: opts.site,
workspace,
Expand Down
4 changes: 1 addition & 3 deletions src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
return Strategy[key] ?? fallback;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
import { isRecord } from './utils.js';

/**
* Discover and register CLI commands.
Expand Down
38 changes: 0 additions & 38 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,13 @@ import { exploreUrl } from './explore.js';
import type { IBrowserFactory } from './runtime.js';
import { synthesizeFromExplore, type SynthesizeCandidateSummary, type SynthesizeResult } from './synthesize.js';

// Registration is a no-op stub — candidates are written to disk by synthesize,
// but not yet auto-copied into the user clis dir.
interface RegisterCandidatesOptions {
target: string;
builtinClis?: string;
userClis?: string;
name?: string;
}

interface RegisterCandidatesResult {
ok: boolean;
count: number;
}

export interface GenerateCliOptions {
url: string;
BrowserFactory: new () => IBrowserFactory;
builtinClis?: string;
userClis?: string;
goal?: string | null;
site?: string;
waitSeconds?: number;
top?: number;
register?: boolean;
workspace?: string;
}

Expand All @@ -57,11 +40,6 @@ export interface GenerateCliResult {
candidate_count: number;
candidates: Array<Pick<SynthesizeCandidateSummary, 'name' | 'strategy' | 'confidence'>>;
};
register: RegisterCandidatesResult | null;
}

function registerCandidates(_opts: RegisterCandidatesOptions): RegisterCandidatesResult {
return { ok: true, count: 0 };
}

const CAPABILITY_ALIASES: Record<string, string[]> = {
Expand Down Expand Up @@ -128,19 +106,6 @@ export async function generateCliFromUrl(opts: GenerateCliOptions): Promise<Gene
const selected = selectCandidate(synthesizeResult.candidates ?? [], opts.goal);
const selectedSite = synthesizeResult.site ?? exploreResult.site;

// Step 4: Register (if requested)
let registerResult: RegisterCandidatesResult | null = null;
if (opts.register !== false && synthesizeResult.candidate_count > 0) {
try {
registerResult = registerCandidates({
target: synthesizeResult.out_dir,
builtinClis: opts.builtinClis,
userClis: opts.userClis,
name: selected?.name,
});
} catch {}
}

const ok = exploreResult.endpoint_count > 0 && synthesizeResult.candidate_count > 0;

return {
Expand All @@ -165,7 +130,6 @@ export async function generateCliFromUrl(opts: GenerateCliOptions): Promise<Gene
confidence: c.confidence,
})),
},
register: registerResult,
};
}

Expand All @@ -189,8 +153,6 @@ export function renderGenerateSummary(r: GenerateCliResult): string {
lines.push(` • ${c.name} (${c.strategy}, ${((c.confidence ?? 0) * 100).toFixed(0)}%)`);
}

if (r.register) lines.push(`\nRegistered: ${r.register.count ?? 0}`);

const fw = r.explore?.framework ?? {};
const fwNames = Object.entries(fw).filter(([, v]) => v).map(([k]) => k);
if (fwNames.length) lines.push(`Framework: ${fwNames.join(', ')}`);
Expand Down
20 changes: 14 additions & 6 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export interface RenderOptions {
footerExtra?: string;
}

function normalizeRows(data: unknown): Record<string, unknown>[] {
return Array.isArray(data) ? data : [data as Record<string, unknown>];
}

function resolveColumns(rows: Record<string, unknown>[], opts: RenderOptions): string[] {
return opts.columns ?? Object.keys(rows[0] ?? {});
}

export function render(data: unknown, opts: RenderOptions = {}): void {
const fmt = opts.fmt ?? 'table';
if (data === null || data === undefined) {
Expand All @@ -31,9 +39,9 @@ export function render(data: unknown, opts: RenderOptions = {}): void {
}

function renderTable(data: unknown, opts: RenderOptions): void {
const rows = Array.isArray(data) ? data : [data as Record<string, unknown>];
const rows = normalizeRows(data);
if (!rows.length) { console.log(chalk.dim('(no data)')); return; }
const columns = opts.columns ?? Object.keys(rows[0]);
const columns = resolveColumns(rows, opts);

const header = columns.map(c => capitalize(c));
const table = new Table({
Expand Down Expand Up @@ -66,9 +74,9 @@ function renderJson(data: unknown): void {
}

function renderMarkdown(data: unknown, opts: RenderOptions): void {
const rows = Array.isArray(data) ? data : [data as Record<string, unknown>];
const rows = normalizeRows(data);
if (!rows.length) return;
const columns = opts.columns ?? Object.keys(rows[0]);
const columns = resolveColumns(rows, opts);
console.log('| ' + columns.join(' | ') + ' |');
console.log('| ' + columns.map(() => '---').join(' | ') + ' |');
for (const row of rows) {
Expand All @@ -77,9 +85,9 @@ function renderMarkdown(data: unknown, opts: RenderOptions): void {
}

function renderCsv(data: unknown, opts: RenderOptions): void {
const rows = Array.isArray(data) ? data : [data as Record<string, unknown>];
const rows = normalizeRows(data);
if (!rows.length) return;
const columns = opts.columns ?? Object.keys(rows[0]);
const columns = resolveColumns(rows, opts);
console.log(columns.join(','));
for (const row of rows) {
console.log(columns.map(c => {
Expand Down
Loading
Loading