From c372551cfa7f177ce7c6a391f458aad094ecc06a Mon Sep 17 00:00:00 2001 From: Oluwaferanmi Oyelude Date: Thu, 11 Jun 2026 16:23:54 -0400 Subject: [PATCH 1/3] feat(cli): add browse clipboard commands Expose the SDK clipboard API through the browse driver and oclif so terminal and agent workflows can read, write, paste, copy, and clear the browser clipboard without raw CDP calls. Co-authored-by: Cursor --- .changeset/browse-clipboard-cli.md | 5 ++ packages/cli/src/commands/clipboard/clear.ts | 24 +++++++ packages/cli/src/commands/clipboard/copy.ts | 24 +++++++ packages/cli/src/commands/clipboard/cut.ts | 24 +++++++ packages/cli/src/commands/clipboard/paste.ts | 35 ++++++++++ packages/cli/src/commands/clipboard/read.ts | 24 +++++++ packages/cli/src/commands/clipboard/write.ts | 37 +++++++++++ .../cli/src/lib/driver/commands/clipboard.ts | 51 ++++++++++++++ .../cli/src/lib/driver/commands/registry.ts | 2 + packages/cli/src/lib/driver/commands/types.ts | 6 ++ packages/cli/tests/clipboard.test.ts | 66 +++++++++++++++++++ packages/cli/tests/driver-commands.test.ts | 2 + 12 files changed, 300 insertions(+) create mode 100644 .changeset/browse-clipboard-cli.md create mode 100644 packages/cli/src/commands/clipboard/clear.ts create mode 100644 packages/cli/src/commands/clipboard/copy.ts create mode 100644 packages/cli/src/commands/clipboard/cut.ts create mode 100644 packages/cli/src/commands/clipboard/paste.ts create mode 100644 packages/cli/src/commands/clipboard/read.ts create mode 100644 packages/cli/src/commands/clipboard/write.ts create mode 100644 packages/cli/src/lib/driver/commands/clipboard.ts create mode 100644 packages/cli/tests/clipboard.test.ts diff --git a/.changeset/browse-clipboard-cli.md b/.changeset/browse-clipboard-cli.md new file mode 100644 index 000000000..63072c191 --- /dev/null +++ b/.changeset/browse-clipboard-cli.md @@ -0,0 +1,5 @@ +--- +"browse": minor +--- + +Add `browse clipboard` commands (read, write, paste, copy, cut, clear) so the browse CLI exposes the SDK clipboard API for agent and terminal workflows. diff --git a/packages/cli/src/commands/clipboard/clear.ts b/packages/cli/src/commands/clipboard/clear.ts new file mode 100644 index 000000000..956278506 --- /dev/null +++ b/packages/cli/src/commands/clipboard/clear.ts @@ -0,0 +1,24 @@ +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardClear extends BrowseCommand { + static override description = + "Clear the browser clipboard for the active page."; + + static override examples = [ + "browse clipboard clear", + "browse clipboard clear --session research", + ]; + + static override flags = { + ...driverCommandFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(ClipboardClear); + await runDriverCommandFromFlags("clipboard.clear", {}, flags); + } +} diff --git a/packages/cli/src/commands/clipboard/copy.ts b/packages/cli/src/commands/clipboard/copy.ts new file mode 100644 index 000000000..8f91ea338 --- /dev/null +++ b/packages/cli/src/commands/clipboard/copy.ts @@ -0,0 +1,24 @@ +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardCopy extends BrowseCommand { + static override description = + "Copy the current selection to the browser clipboard on the active page."; + + static override examples = [ + "browse clipboard copy", + "browse clipboard copy --session research", + ]; + + static override flags = { + ...driverCommandFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(ClipboardCopy); + await runDriverCommandFromFlags("clipboard.copy", {}, flags); + } +} diff --git a/packages/cli/src/commands/clipboard/cut.ts b/packages/cli/src/commands/clipboard/cut.ts new file mode 100644 index 000000000..ef3ae2329 --- /dev/null +++ b/packages/cli/src/commands/clipboard/cut.ts @@ -0,0 +1,24 @@ +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardCut extends BrowseCommand { + static override description = + "Cut the current selection to the browser clipboard on the active page."; + + static override examples = [ + "browse clipboard cut", + "browse clipboard cut --session research", + ]; + + static override flags = { + ...driverCommandFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(ClipboardCut); + await runDriverCommandFromFlags("clipboard.cut", {}, flags); + } +} diff --git a/packages/cli/src/commands/clipboard/paste.ts b/packages/cli/src/commands/clipboard/paste.ts new file mode 100644 index 000000000..8c019e21c --- /dev/null +++ b/packages/cli/src/commands/clipboard/paste.ts @@ -0,0 +1,35 @@ +import { Flags } from "@oclif/core"; + +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardPaste extends BrowseCommand { + static override description = + "Paste clipboard text into the focused field on the active page."; + + static override examples = [ + "browse clipboard paste", + "browse clipboard paste --shortcut Control+V", + ]; + + static override flags = { + ...driverCommandFlags, + shortcut: Flags.string({ + description: "Keyboard shortcut to trigger paste.", + helpValue: "", + options: ["ControlOrMeta+V", "Meta+V", "Control+V"], + }), + }; + + async run(): Promise { + const { flags } = await this.parse(ClipboardPaste); + await runDriverCommandFromFlags( + "clipboard.paste", + { shortcut: flags.shortcut }, + flags, + ); + } +} diff --git a/packages/cli/src/commands/clipboard/read.ts b/packages/cli/src/commands/clipboard/read.ts new file mode 100644 index 000000000..04443010f --- /dev/null +++ b/packages/cli/src/commands/clipboard/read.ts @@ -0,0 +1,24 @@ +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardRead extends BrowseCommand { + static override description = + "Read text from the browser clipboard for the active page."; + + static override examples = [ + "browse clipboard read", + "browse clipboard read --session research", + ]; + + static override flags = { + ...driverCommandFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(ClipboardRead); + await runDriverCommandFromFlags("clipboard.read", {}, flags); + } +} diff --git a/packages/cli/src/commands/clipboard/write.ts b/packages/cli/src/commands/clipboard/write.ts new file mode 100644 index 000000000..fe03801c7 --- /dev/null +++ b/packages/cli/src/commands/clipboard/write.ts @@ -0,0 +1,37 @@ +import { Args } from "@oclif/core"; + +import { BrowseCommand } from "../../base.js"; +import { + driverCommandFlags, + runDriverCommandFromFlags, +} from "../../lib/driver/command-cli.js"; + +export default class ClipboardWrite extends BrowseCommand { + static override description = + "Write text to the browser clipboard for the active page."; + + static override examples = [ + "browse clipboard write 'hello world'", + "browse clipboard write 'seed text' --session research", + ]; + + static override args = { + text: Args.string({ + description: "Text to write to the clipboard.", + required: true, + }), + }; + + static override flags = { + ...driverCommandFlags, + }; + + async run(): Promise { + const { args, flags } = await this.parse(ClipboardWrite); + await runDriverCommandFromFlags( + "clipboard.write", + { text: args.text }, + flags, + ); + } +} diff --git a/packages/cli/src/lib/driver/commands/clipboard.ts b/packages/cli/src/lib/driver/commands/clipboard.ts new file mode 100644 index 000000000..c25148490 --- /dev/null +++ b/packages/cli/src/lib/driver/commands/clipboard.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import type { DriverCommandHandlers } from "./types.js"; + +const writeParamsSchema = z.object({ + text: z.string(), +}); + +const pasteParamsSchema = z.object({ + shortcut: z.enum(["ControlOrMeta+V", "Meta+V", "Control+V"]).optional(), +}); + +export const clipboardHandlers: DriverCommandHandlers = { + async "clipboard.read"(manager) { + const context = await manager.browserContext(); + const text = await context.clipboard.readText(); + return { text }; + }, + + async "clipboard.write"(manager, params) { + const { text } = writeParamsSchema.parse(params); + const context = await manager.browserContext(); + await context.clipboard.writeText(text); + return { ok: true }; + }, + + async "clipboard.clear"(manager) { + const context = await manager.browserContext(); + await context.clipboard.clear(); + return { ok: true }; + }, + + async "clipboard.paste"(manager, params) { + const { shortcut } = pasteParamsSchema.parse(params ?? {}); + const context = await manager.browserContext(); + await context.clipboard.paste(shortcut ? { shortcut } : undefined); + return { ok: true }; + }, + + async "clipboard.copy"(manager) { + const context = await manager.browserContext(); + await context.clipboard.copy(); + return { ok: true }; + }, + + async "clipboard.cut"(manager) { + const context = await manager.browserContext(); + await context.clipboard.cut(); + return { ok: true }; + }, +}; diff --git a/packages/cli/src/lib/driver/commands/registry.ts b/packages/cli/src/lib/driver/commands/registry.ts index c6baeab06..55d46afb2 100644 --- a/packages/cli/src/lib/driver/commands/registry.ts +++ b/packages/cli/src/lib/driver/commands/registry.ts @@ -1,4 +1,5 @@ import type { DriverSessionManager } from "../session-manager.js"; +import { clipboardHandlers } from "./clipboard.js"; import { elementsHandlers } from "./elements.js"; import { keyboardHandlers } from "./keyboard.js"; import { mouseHandlers } from "./mouse.js"; @@ -12,6 +13,7 @@ import type { DriverCommandHandlers, DriverCommandName } from "./types.js"; const handlers: DriverCommandHandlers = { ...navigationHandlers, + ...clipboardHandlers, ...elementsHandlers, ...keyboardHandlers, ...mouseHandlers, diff --git a/packages/cli/src/lib/driver/commands/types.ts b/packages/cli/src/lib/driver/commands/types.ts index b72480f2c..5f64df208 100644 --- a/packages/cli/src/lib/driver/commands/types.ts +++ b/packages/cli/src/lib/driver/commands/types.ts @@ -4,6 +4,12 @@ import { z } from "zod"; export const DRIVER_COMMAND_NAMES = [ "back", "click", + "clipboard.clear", + "clipboard.copy", + "clipboard.cut", + "clipboard.paste", + "clipboard.read", + "clipboard.write", "cursor", "eval", "fill", diff --git a/packages/cli/tests/clipboard.test.ts b/packages/cli/tests/clipboard.test.ts new file mode 100644 index 000000000..b45226db0 --- /dev/null +++ b/packages/cli/tests/clipboard.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; + +import { clipboardHandlers } from "../src/lib/driver/commands/clipboard.js"; +import type { DriverSessionManager } from "../src/lib/driver/session-manager.js"; + +function makeManager(clipboard: Record>) { + return { + browserContext: vi.fn().mockResolvedValue({ clipboard }), + } as unknown as DriverSessionManager; +} + +describe("clipboard driver handlers", () => { + it("reads clipboard text", async () => { + const readText = vi.fn().mockResolvedValue("copied value"); + const manager = makeManager({ readText }); + + await expect( + clipboardHandlers["clipboard.read"]!(manager, {}), + ).resolves.toEqual({ text: "copied value" }); + expect(readText).toHaveBeenCalledOnce(); + }); + + it("writes clipboard text", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ writeText }); + + await expect( + clipboardHandlers["clipboard.write"]!(manager, { text: "hello" }), + ).resolves.toEqual({ ok: true }); + expect(writeText).toHaveBeenCalledWith("hello"); + }); + + it("pastes with an optional shortcut", async () => { + const paste = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ paste }); + + await expect( + clipboardHandlers["clipboard.paste"]!(manager, { + shortcut: "Control+V", + }), + ).resolves.toEqual({ ok: true }); + expect(paste).toHaveBeenCalledWith({ shortcut: "Control+V" }); + }); + + it("clears, copies, and cuts via clipboard helpers", async () => { + const clear = vi.fn().mockResolvedValue(undefined); + const copy = vi.fn().mockResolvedValue(undefined); + const cut = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ clear, copy, cut }); + + await expect( + clipboardHandlers["clipboard.clear"]!(manager, {}), + ).resolves.toEqual({ ok: true }); + await expect( + clipboardHandlers["clipboard.copy"]!(manager, {}), + ).resolves.toEqual({ ok: true }); + await expect( + clipboardHandlers["clipboard.cut"]!(manager, {}), + ).resolves.toEqual({ + ok: true, + }); + expect(clear).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledOnce(); + expect(cut).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/cli/tests/driver-commands.test.ts b/packages/cli/tests/driver-commands.test.ts index baecd003b..fdc7d9f3d 100644 --- a/packages/cli/tests/driver-commands.test.ts +++ b/packages/cli/tests/driver-commands.test.ts @@ -25,6 +25,8 @@ describe("driver commands", () => { "snapshot", "tab.switch", "network.on", + "clipboard.read", + "clipboard.write", "upload", "viewport", ]), From ebaaf0d15d299f03a2dac689a255e04904f03db9 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 11 Jun 2026 14:54:11 -0700 Subject: [PATCH 2/3] chore: retrigger CI (claimed-PR workflows did not fire) From 2acce35ce187d1889b90dab27a26d5f25a7ef04b Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 11 Jun 2026 15:30:27 -0700 Subject: [PATCH 3/3] fix(cli): clarify clipboard scoping in help text, add validation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derisked per review: context.clipboard drives navigator.clipboard in the target browser, so scope depends on the target — Browserbase remote and managed headless local sessions get an isolated per-browser clipboard, while headed local sessions and --cdp attachments share the machine's OS clipboard. Command descriptions previously claimed page-level scoping. Also adds unit tests for the zod validation paths (write without text, non-string text, unsupported paste shortcut, paste with no options) and registers a clipboard oclif topic description. Co-Authored-By: Claude Fable 5 --- packages/cli/package.json | 3 ++ packages/cli/src/commands/clipboard/clear.ts | 4 +- packages/cli/src/commands/clipboard/copy.ts | 4 +- packages/cli/src/commands/clipboard/cut.ts | 4 +- packages/cli/src/commands/clipboard/paste.ts | 4 +- packages/cli/src/commands/clipboard/read.ts | 4 +- packages/cli/src/commands/clipboard/write.ts | 4 +- packages/cli/src/lib/driver/command-cli.ts | 3 ++ packages/cli/tests/clipboard.test.ts | 40 ++++++++++++++++++++ 9 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index b7fcc0cda..e4932b4d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -31,6 +31,9 @@ }, "topicSeparator": " ", "topics": { + "clipboard": { + "description": "Read, write, and interact with the browser session clipboard." + }, "cloud": { "description": "Manage Browserbase cloud resources and APIs." }, diff --git a/packages/cli/src/commands/clipboard/clear.ts b/packages/cli/src/commands/clipboard/clear.ts index 956278506..d657eede0 100644 --- a/packages/cli/src/commands/clipboard/clear.ts +++ b/packages/cli/src/commands/clipboard/clear.ts @@ -1,12 +1,12 @@ import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardClear extends BrowseCommand { - static override description = - "Clear the browser clipboard for the active page."; + static override description = `Clear the clipboard for the active browser session.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard clear", diff --git a/packages/cli/src/commands/clipboard/copy.ts b/packages/cli/src/commands/clipboard/copy.ts index 8f91ea338..732f80f9f 100644 --- a/packages/cli/src/commands/clipboard/copy.ts +++ b/packages/cli/src/commands/clipboard/copy.ts @@ -1,12 +1,12 @@ import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardCopy extends BrowseCommand { - static override description = - "Copy the current selection to the browser clipboard on the active page."; + static override description = `Copy the current page selection to the session clipboard.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard copy", diff --git a/packages/cli/src/commands/clipboard/cut.ts b/packages/cli/src/commands/clipboard/cut.ts index ef3ae2329..25bf39b5e 100644 --- a/packages/cli/src/commands/clipboard/cut.ts +++ b/packages/cli/src/commands/clipboard/cut.ts @@ -1,12 +1,12 @@ import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardCut extends BrowseCommand { - static override description = - "Cut the current selection to the browser clipboard on the active page."; + static override description = `Cut the current page selection to the session clipboard.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard cut", diff --git a/packages/cli/src/commands/clipboard/paste.ts b/packages/cli/src/commands/clipboard/paste.ts index 8c019e21c..7e491ef46 100644 --- a/packages/cli/src/commands/clipboard/paste.ts +++ b/packages/cli/src/commands/clipboard/paste.ts @@ -2,13 +2,13 @@ import { Flags } from "@oclif/core"; import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardPaste extends BrowseCommand { - static override description = - "Paste clipboard text into the focused field on the active page."; + static override description = `Paste session clipboard text into the focused field on the active page.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard paste", diff --git a/packages/cli/src/commands/clipboard/read.ts b/packages/cli/src/commands/clipboard/read.ts index 04443010f..61b7ac03e 100644 --- a/packages/cli/src/commands/clipboard/read.ts +++ b/packages/cli/src/commands/clipboard/read.ts @@ -1,12 +1,12 @@ import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardRead extends BrowseCommand { - static override description = - "Read text from the browser clipboard for the active page."; + static override description = `Read text from the session clipboard.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard read", diff --git a/packages/cli/src/commands/clipboard/write.ts b/packages/cli/src/commands/clipboard/write.ts index fe03801c7..c10bca11e 100644 --- a/packages/cli/src/commands/clipboard/write.ts +++ b/packages/cli/src/commands/clipboard/write.ts @@ -2,13 +2,13 @@ import { Args } from "@oclif/core"; import { BrowseCommand } from "../../base.js"; import { + clipboardScopeNote, driverCommandFlags, runDriverCommandFromFlags, } from "../../lib/driver/command-cli.js"; export default class ClipboardWrite extends BrowseCommand { - static override description = - "Write text to the browser clipboard for the active page."; + static override description = `Write text to the session clipboard.\n\n${clipboardScopeNote}`; static override examples = [ "browse clipboard write 'hello world'", diff --git a/packages/cli/src/lib/driver/command-cli.ts b/packages/cli/src/lib/driver/command-cli.ts index 18c442f55..008ff21c7 100644 --- a/packages/cli/src/lib/driver/command-cli.ts +++ b/packages/cli/src/lib/driver/command-cli.ts @@ -18,6 +18,9 @@ import type { ConnectionTarget } from "./types.js"; import { outputJson } from "../output.js"; import { runDriverCommandWithTarget } from "./runtime.js"; +export const clipboardScopeNote = + "Clipboard scope depends on the session target: Browserbase remote sessions and managed headless local sessions use an isolated per-browser clipboard, while headed local sessions and --cdp attachments share the machine's OS clipboard with every other app and session on that machine."; + export const driverCommandFlags = { "auto-connect": autoConnectFlag, cdp: cdpFlag, diff --git a/packages/cli/tests/clipboard.test.ts b/packages/cli/tests/clipboard.test.ts index b45226db0..606f0b877 100644 --- a/packages/cli/tests/clipboard.test.ts +++ b/packages/cli/tests/clipboard.test.ts @@ -63,4 +63,44 @@ describe("clipboard driver handlers", () => { expect(copy).toHaveBeenCalledOnce(); expect(cut).toHaveBeenCalledOnce(); }); + + it("rejects write without text", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ writeText }); + + await expect( + clipboardHandlers["clipboard.write"]!(manager, {}), + ).rejects.toThrow(); + expect(writeText).not.toHaveBeenCalled(); + }); + + it("rejects write with non-string text", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ writeText }); + + await expect( + clipboardHandlers["clipboard.write"]!(manager, { text: 42 }), + ).rejects.toThrow(); + expect(writeText).not.toHaveBeenCalled(); + }); + + it("rejects paste with an unsupported shortcut", async () => { + const paste = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ paste }); + + await expect( + clipboardHandlers["clipboard.paste"]!(manager, { shortcut: "Alt+V" }), + ).rejects.toThrow(); + expect(paste).not.toHaveBeenCalled(); + }); + + it("pastes without options when no shortcut is given", async () => { + const paste = vi.fn().mockResolvedValue(undefined); + const manager = makeManager({ paste }); + + await expect( + clipboardHandlers["clipboard.paste"]!(manager, undefined), + ).resolves.toEqual({ ok: true }); + expect(paste).toHaveBeenCalledWith(undefined); + }); });