import type { Command } from "commander"; import { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "../../browser/paths.js"; import { danger } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; import { shortenHomePath } from "../../utils.js"; import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js"; import { resolveBrowserActionContext } from "./shared.js"; async function normalizeUploadPaths(paths: string[]): Promise { const result = await resolveExistingPathsWithinRoot({ rootDir: DEFAULT_UPLOAD_DIR, requestedPaths: paths, scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`, }); if (!result.ok) { throw new Error(result.error); } return result.paths; } async function runBrowserPostAction(params: { parent: BrowserParentOpts; profile: string | undefined; path: string; body: Record; timeoutMs: number; describeSuccess: (result: T) => string; }): Promise { try { const result = await callBrowserRequest( params.parent, { method: "POST", path: params.path, query: params.profile ? { profile: params.profile } : undefined, body: params.body, }, { timeoutMs: params.timeoutMs }, ); if (params.parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } defaultRuntime.log(params.describeSuccess(result)); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } } export function registerBrowserFilesAndDownloadsCommands( browser: Command, parentOpts: (cmd: Command) => BrowserParentOpts, ) { const resolveTimeoutAndTarget = (opts: { timeoutMs?: unknown; targetId?: unknown }) => { const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : undefined; const targetId = typeof opts.targetId === "string" ? opts.targetId.trim() || undefined : undefined; return { timeoutMs, targetId }; }; const runDownloadCommand = async ( cmd: Command, opts: { timeoutMs?: unknown; targetId?: unknown }, request: { path: string; body: Record }, ) => { const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts); const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts); await runBrowserPostAction<{ download: { path: string } }>({ parent, profile, path: request.path, body: { ...request.body, targetId, timeoutMs, }, timeoutMs: timeoutMs ?? 20000, describeSuccess: (result) => `downloaded: ${shortenHomePath(result.download.path)}`, }); }; browser .command("upload") .description("Arm file upload for the next file chooser") .argument( "", "File paths to upload (must be within OpenClaw temp uploads dir, e.g. /tmp/openclaw/uploads/file.pdf)", ) .option("--ref ", "Ref id from snapshot to click after arming") .option("--input-ref ", "Ref id for to set directly") .option("--element ", "CSS selector for ") .option("--target-id ", "CDP target id (or unique prefix)") .option( "--timeout-ms ", "How long to wait for the next file chooser (default: 120000)", (v: string) => Number(v), ) .action(async (paths: string[], opts, cmd) => { const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts); const normalizedPaths = await normalizeUploadPaths(paths); const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts); await runBrowserPostAction({ parent, profile, path: "/hooks/file-chooser", body: { paths: normalizedPaths, ref: opts.ref?.trim() || undefined, inputRef: opts.inputRef?.trim() || undefined, element: opts.element?.trim() || undefined, targetId, timeoutMs, }, timeoutMs: timeoutMs ?? 20000, describeSuccess: () => `upload armed for ${paths.length} file(s)`, }); }); browser .command("waitfordownload") .description("Wait for the next download (and save it)") .argument( "[path]", "Save path within openclaw temp downloads dir (default: /tmp/openclaw/downloads/...; fallback: os.tmpdir()/openclaw/downloads/...)", ) .option("--target-id ", "CDP target id (or unique prefix)") .option( "--timeout-ms ", "How long to wait for the next download (default: 120000)", (v: string) => Number(v), ) .action(async (outPath: string | undefined, opts, cmd) => { await runDownloadCommand(cmd, opts, { path: "/wait/download", body: { path: outPath?.trim() || undefined, }, }); }); browser .command("download") .description("Click a ref and save the resulting download") .argument("", "Ref id from snapshot to click") .argument( "", "Save path within openclaw temp downloads dir (e.g. report.pdf or /tmp/openclaw/downloads/report.pdf)", ) .option("--target-id ", "CDP target id (or unique prefix)") .option( "--timeout-ms ", "How long to wait for the download to start (default: 120000)", (v: string) => Number(v), ) .action(async (ref: string, outPath: string, opts, cmd) => { await runDownloadCommand(cmd, opts, { path: "/download", body: { ref, path: outPath, }, }); }); browser .command("dialog") .description("Arm the next modal dialog (alert/confirm/prompt)") .option("--accept", "Accept the dialog", false) .option("--dismiss", "Dismiss the dialog", false) .option("--prompt ", "Prompt response text") .option("--target-id ", "CDP target id (or unique prefix)") .option( "--timeout-ms ", "How long to wait for the next dialog (default: 120000)", (v: string) => Number(v), ) .action(async (opts, cmd) => { const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts); const accept = opts.accept ? true : opts.dismiss ? false : undefined; if (accept === undefined) { defaultRuntime.error(danger("Specify --accept or --dismiss")); defaultRuntime.exit(1); return; } const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts); await runBrowserPostAction({ parent, profile, path: "/hooks/dialog", body: { accept, promptText: opts.prompt?.trim() || undefined, targetId, timeoutMs, }, timeoutMs: timeoutMs ?? 20000, describeSuccess: () => "dialog armed", }); }); }