import { Cli, z } from "incur"; import { browserContext, browserCliError } from "../cliBrowserContext.js"; import { browserScopeOptions } from "../cliObservationOptions.js"; import { captureWebScreenshot, compareWebCaptureEvidence, compareWebScreenshotEvidence, discoverWebMcpTools, } from "../application/BrowserObservationService.js"; import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js"; import { logCliCommand } from "../cliLogging.js"; import { AnalysisInputError, type AnalysisInputIssue, } from "../domain/analysisErrorCore.js"; import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js"; import { discoverWebMcpToolsInputSchema } from "../domain/webMcpDiscovery.js"; import { captureWebScreenshotInputSchema, compareWebScreenshotsInputSchema, } from "../domain/webScreenshot.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { safeParseJson } from "../domain/safeJson.js"; import type { Logger } from "../logger.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; /** Register WebMCP, capture-diff, and screenshot CLI equivalents. */ export const registerAdvancedBrowserCommands = ( cli: ReturnType, logger: Logger, ): void => { registerWebMcp(cli, logger); registerCaptureDiff(cli, logger); registerScreenshot(cli, logger); registerScreenshotDiff(cli, logger); }; const registerWebMcp = ( cli: ReturnType, logger: Logger, ): void => { cli.command(CLI_COMMANDS.discoverWebMcpTools, { description: "Passively discover page-declared WebMCP tools", args: z.object({ endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"), targetId: z.string().describe("Target ID from list-browser-targets"), }), options: z.object({ ...browserScopeOptions, observationMs: z .number() .int() .min(0) .default(100) .describe("Observation duration in milliseconds"), }), run: ({ args, options }) => logCliCommand(logger, "discover-webmcp-tools", async () => { const context = browserContext(); const parsed = discoverWebMcpToolsInputSchema.safeParse({ cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, target_id: args.targetId, observation_ms: options.observationMs, }); if (!parsed.success) return inputError("discover_webmcp_tools"); const result = await discoverWebMcpTools(context.provider, parsed.data); return result.ok ? result.value : browserCliError(result.error); }), }); }; const registerCaptureDiff = ( cli: ReturnType, logger: Logger, ): void => { cli.command(CLI_COMMANDS.compareWebCaptures, { description: "Compare two normalized passive captures or recorded browser scenarios", args: z.object({ beforeJson: z.string().describe("Earlier normalized web capture JSON"), afterJson: z.string().describe("Later normalized web capture JSON"), }), options: z.object({ normalizationJson: z .string() .default('{"rules":[]}') .describe( "Recorded literal normalization policy for browser scenarios", ), }), run: ({ args, options }) => logCliCommand(logger, "compare-web-captures", async () => { const before = safeParseJson(args.beforeJson); const after = safeParseJson(args.afterJson); const normalization = safeParseJson(options.normalizationJson); if (!before.ok) return inputError("compare_web_captures", [ invalidJsonIssue("before"), ]); if (!after.ok) return inputError("compare_web_captures", [ invalidJsonIssue("after"), ]); if (!normalization.ok) return inputError("compare_web_captures", [ invalidJsonIssue("normalization"), ]); const scenarioComparison = browserCaptureComparisonInputSchema.safeParse({ before_scenario: before.value, after_scenario: after.value, normalization: normalization.value, }); const parsed = scenarioComparison.success ? scenarioComparison : browserCaptureComparisonInputSchema.safeParse({ before: before.value, after: after.value, }); if (!parsed.success) return inputError("compare_web_captures"); const result = await compareWebCaptureEvidence( new CdpBrowserProvider(), parsed.data, ); return result.ok ? result.value : browserCliError(result.error); }), }); }; const registerScreenshot = ( cli: ReturnType, logger: Logger, ): void => { cli.command(CLI_COMMANDS.captureWebScreenshot, { description: "Capture one visible page viewport within the supplied origin scope", args: z.object({ endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"), targetId: z.string().describe("Target ID from list-browser-targets"), }), options: z.object({ ...browserScopeOptions, }), run: ({ args, options }) => logCliCommand(logger, "capture-web-screenshot", async () => { const context = browserContext(); const parsed = captureWebScreenshotInputSchema.safeParse({ cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, target_id: args.targetId, }); if (!parsed.success) return inputError("capture_web_screenshot"); const result = await captureWebScreenshot( context.provider, parsed.data, ); return result.ok ? result.value : browserCliError(result.error); }), }); }; const registerScreenshotDiff = ( cli: ReturnType, logger: Logger, ): void => { cli.command(CLI_COMMANDS.compareWebScreenshots, { description: "Compare two self-verifying PNG artifact JSON values", args: z.object({ beforeJson: z.string().describe("Earlier screenshot artifact JSON"), afterJson: z.string().describe("Later screenshot artifact JSON"), }), options: z.object({ channelThreshold: z .number() .int() .min(0) .max(255) .default(0) .describe("Per-channel difference threshold for changed pixels"), }), run: ({ args, options }) => logCliCommand(logger, "compare-web-screenshots", async () => { const parsed = compareWebScreenshotsInputSchema.safeParse({ before: parseJson(args.beforeJson), after: parseJson(args.afterJson), channel_threshold: options.channelThreshold, }); if (!parsed.success) return inputError("compare_web_screenshots"); const result = await compareWebScreenshotEvidence( new CdpBrowserProvider(), parsed.data, ); return result.ok ? result.value : browserCliError(result.error); }), }); }; const parseJson = (value: string): unknown => { const parsed = safeParseJson(value); return parsed.ok ? parsed.value : undefined; }; const invalidJsonIssue = (field: string): AnalysisInputIssue => ({ path: [field], reason: "invalid_format", expected: "JSON", }); const inputError = ( operation: string, issues: readonly AnalysisInputIssue[] = [], ): JsonValue => browserCliError(new AnalysisInputError(operation, undefined, issues));