Skip to content

Latest commit

 

History

History
439 lines (304 loc) · 27.8 KB

cli.md

File metadata and controls

439 lines (304 loc) · 27.8 KB

Stylebot CLI

A command line for driving Stylebot in a running browser: list tabs, read and inspect pages, hover and pick elements, force hover and focus states, get and set styles, and take screenshots. It's meant for scripting and for coding agents such as Claude Code, Codex or Cursor, which can restyle a page and check the result without the editor open.

It works with Stylebot on Chrome, Edge and the other Chromium browsers listed below, on macOS, Linux and Windows, once turned on in Stylebot's options. Firefox and Safari aren't supported yet.

Setup

The CLI needs Node 20 or later, and Stylebot 4 or later in the browser. It's published to npm as @stylebot/cli:

npm install -g @stylebot/cli
stylebot install

To run this checkout's CLI instead, use yarn stylebot inside the repo. To run it as stylebot from anywhere on macOS or Linux, link it onto your PATH:

ln -s "$PWD/tools/cli/bin/stylebot.mjs" ~/bin/stylebot

On Windows, run yarn stylebot or node tools\cli\lib\cli.mjs.

The first command sets the CLI up with your browsers on its own, saying which. To check setup, or after installing another browser, run install; for this checkout's CLI, run it from the repo so it finds the dev profiles:

yarn stylebot install

This copies the CLI's native messaging host into ~/.stylebot/ and registers it with each installed browser, then prints a checklist of what's done and what's left: the browsers it found, whether Stylebot is in one of them, and whether it's connected. Run it again any time to check. On macOS and Linux it also registers with the dev profiles yarn dev:chrome launches, which keep their own list of hosts; on Windows the browser's registration covers them.

Browser macOS Linux Windows
Chrome ✓ ✓ ✓
Edge ✓ ✓ ✓
Brave ✓ ✓ ✓
Chromium ✓ ✓ ✓
Vivaldi ✓ ✓ ✓
Arc ✓

A browser counts as installed once it has run, which leaves a Local State file in its user data folder, so run install again after installing a new browser.

Then turn on Allow command line access in Stylebot's options, under Basics, and allow what the browser asks for. The extension connects to the host while the setting is on; turning it off disconnects it and gives back the permission to talk to the host. If it was already on before you installed, reload the extension or restart the browser. The host runs only while the browser does.

When it can't connect

A command that can't reach the host works out why and prints one fix:

  • No browser could be set up: install Chrome or Edge.
  • No browser profile has Stylebot: add it. The CLI looks for Stylebot's store ids in each profile's extensions and preferences, which covers unpacked dev builds too, and says this only when it found profiles to look in.
  • The host is registered but no browser has ever started it, so the setting is almost certainly off: turn it on. The host leaves a timestamp in ~/.stylebot/ each time a browser starts it, one per socket, so a test browser's doesn't count for yours.
  • A browser has started it before: the browser is closed, or Stylebot isn't running in it. Open it.

The Stylebot skill relays these messages to you as they are.

Commands

stylebot --help lists every command, and stylebot <command> --help describes one.

Naming the target

Commands that only read default to the active tab, or take --tab when their arguments are selectors. Commands that change a style, take a screenshot, move the pointer or force a state (css set, css rule, css add, profile, screenshot, hover, inspect, force) need the site or tab named, because an agent runs its commands over several steps and the active tab can change in between.

  • A tab is a tab id, from tabs or open.
  • A target is a tab id or a site, such as news.ycombinator.com.
  • A profile is named by its name or id.

Several browsers

Each browser running Stylebot with the setting on connects on its own, and so does each profile of one browser. With one connected, commands go to it. With more, every command takes --browser to say which, since each keeps its own styles and tabs:

yarn stylebot browsers
yarn stylebot open news.ycombinator.com --browser edge
  • browsers lists them, each with the page it shows, to tell them apart. A browser goes by its name, such as chrome or edge, or by its name and a short id, such as chrome-k3f9, when two of its profiles are connected.
  • A command run on a terminal without --browser asks which browser to use. Run elsewhere, such as by an agent, it fails, listing the browsers to choose from. The /stylebot skill then asks you which one, and names it in every command after.

Pages

stylebot open news.ycombinator.com
stylebot outline 54432929
stylebot screenshot 54432929 -o page.png
stylebot screenshot 54432929 --selector '.titleline > a' -o link.png
stylebot screenshot 54432929 --clip 0,0,600,400 --scale 2 -o corner.png
stylebot screenshot 54432929 --state hover --on '.athing' --selector '.athing' -o row.png
stylebot done
  • open shows the page in the CLI's own window, which opens behind yours without taking focus, so an agent never switches your tabs. It reuses that window's tab for the page (any page on the site, for a bare site); --new always opens another.
  • tabs lists open tabs, starring the one each window shows.
  • outline prints the page's visible elements as an indented outline.
  • screenshot saves a PNG of a tab's viewport. A hidden tab is shown for a moment to capture it, then the window switches back. It prints the file, the image's size and its scale.
    • --selector keeps only the first visible element a selector matches, with a few pixels around it. One out of view is scrolled to for the capture, and the page scrolled back after. One bigger than the viewport is cut to the part that fits, and the output says so. Checking one element this way is quicker and smaller than a whole page.
    • --clip x,y,w,h keeps a region, in CSS pixels of the viewport. It can't be combined with --selector.
    • --scale sets image pixels per CSS pixel. The default, 1, makes a point in a full screenshot the CSS pixel hover and inspect take; 2 keeps a 2× screen's full resolution.
    • --state <state> --on '<selector>' forces a state on the selector's first visible match for this capture only, as force does, then takes it off again, unless it was already forced.
  • done closes the CLI's window.

Inspecting a page

stylebot suggestions 54432929
stylebot css-variables 54432929
stylebot page-rules '.titleline > a' --tab 54432929
stylebot computed-styles body color font-size --tab 54432929
stylebot match-count '.titleline > a' '.subline' --tab 54432929

These read the page so an agent can look at a page before styling it without another browser tool.

  • suggestions prints styling requests for the page: up to two that suit it, and a creative look.
  • css-variables prints the page's css variables, as the :root and body rules that set them, so a style can override them.
  • page-rules prints the page's own rules for the elements a selector matches (the first five). Cross-origin stylesheets can't be read, so they're only counted.
  • computed-styles prints computed values on the first element a selector matches: the properties named, or common layout, color and type ones.
  • match-count prints how many elements each selector matches, and invalid for one that doesn't parse.

Pointer

stylebot hover 54432929 '#nav .menu'
stylebot inspect 54432929 120,340

These point at the page, so an agent can open what only shows on hover, and find the selector for an element it sees in a screenshot. Neither clicks anything or draws on the page, so screenshots stay clean. Each needs the tab named.

  • hover moves the pointer onto an element, or a point given as x,y in CSS pixels of the viewport, as read off a full screenshot at the default scale. A selector aims at the middle of its first visible match, scrolled into view.
  • inspect prints the selector Stylebot's inspector would pick for an element or point, preferring one the site's style already has rules for. Below it come the other selectors the editor's selector menu would offer for the element, each with how many elements it matches, so an agent can pick a narrower or broader one. It sends the page no events and doesn't open the editor.

Hover's events are synthetic: the page's own handlers run, but CSS :hover doesn't apply, and a site that checks for a real user can ignore them. To see hover styles, force the state.

Forced states

stylebot force 54432929 '.athing' hover
stylebot force 54432929 'input[name=q]' focus-visible
stylebot force 54432929
stylebot force 54432929 --clear

force <tab> '<selector>' <state> makes the page's CSS for a state apply as if a real pointer or focus were there: hover, active, focus, focus-visible or focus-within. It goes on the selector's first visible match, or every match with --all, and stays until --clear. While it's forced, outline, computed-styles, page-rules, inspect, the page check after a save and screenshot all see the page in that state, so an agent can check a theme's hover and focus colors instead of reading them off the CSS. force <tab> lists what's forced.

$ stylebot force 1234 '.athing' hover
Forced hover on .athing (1 element, 6 ancestors).

It works the way Storybook's pseudo-states addon does, without the browser's debugger, so it needs no extra permission:

  • It copies every rule on the page that names one of those states, from the page's stylesheets, its constructed ones and those of open shadow roots, with the state swapped for an attribute of the same specificity, inside the same @media, @supports, @layer and @container, !important kept. The copies go after the scope's own sheets. A cross-origin stylesheet the page can't read is fetched by the extension, which says when it couldn't fetch one.
  • Hover and active go on the element and every ancestor, across shadow boundaries; a focus state goes on the element, with focus-within on its ancestors. Transitions the change starts jump to their end, so a screenshot shows the state rather than part of the fade.
  • While anything is forced, it copies the rules again when stylesheets are added or change, including Stylebot's own when a save applies, and before every command reads the page.

Its limits:

  • Only the top frame is forced, as the CLI only reads that one; iframes keep their real state.
  • A forced state doesn't stop the page's rules for when an element isn't in that state, such as .button:not(:hover), from applying to it too.
  • The copies come after every other rule, so where a state rule and a later rule tie on specificity without !important, the state rule wins, where with a real pointer the later one would. Stylebot's own rules carry !important unless the site turns that off, so they still win.
  • An element the page replaces, as a framework re-render can, loses its forced state; force again.

Styles

stylebot css get news.ycombinator.com --grep titleline
stylebot css rule 54432929 '.titleline a' --set color=#e8e6e3
stylebot css add 54432929 --file more.css
stylebot css set 54432929 < hn.css
  • styles lists saved styles.

  • css get prints a style's css. --grep <text> prints only the rules whose selector or declarations contain the text, ignoring case, inside the at-rules they sit in.

  • css set saves css from stdin or --file in place of the whole stylesheet and applies it to open tabs right away. Empty css deletes the style.

  • css rule <target> '<selector>' edits the first top-level rule with exactly that selector, or with --media '<query>' the first inside that top-level @media. --set property=value sets a declaration and --unset property removes one, each repeatable; --delete removes the rule. Setting creates the rule at the end when none matches, and a rule left empty is removed. Everything else in the stylesheet keeps its formatting and comments.

  • css add adds css from stdin or --file to the end of the stylesheet, moving any @import to the top, and names the rules it added whose selector already has one, which css rule could have edited instead.

  • css diff shows the rules css from stdin or --file would add, change and remove if css set saved it, without saving anything.

  • css rule and css add save the way css set does, with the same checks and report:

    • It refuses css that doesn't parse, saying at which line, and adds Google Fonts imports for the families the css names. It also saves a selector's partly generated class names by their stable part, and says which it changed.

    • When the profile it saves is the one in use and an open tab shows the site, it checks the page; otherwise it says why it didn't. Given a tab id, it saves the style that applies on that tab and checks that tab. Given a site, it checks the active tab of the CLI's own window if it shows the site, else any tab that does, and names the other tabs showing the site in one line.

      $ stylebot css set news.ycombinator.com --file hn.css
      Saved news.ycombinator.com
      Checked on tab 1234.
      Also shown on tab 1240: name a tab id instead of the site to check that tab.
      Elements each added or changed rule matched:
      - .titleline a: 30 elements
      Removed rules:
      - .old-banner
      5 unchanged rules not listed
      Problems the page check found:
      - Hard to read: .subtext (30 elements), text #828282 on #1d1f21, contrast 3.1:1
      
    • css rule says when it created or removed the rule, and names the properties it was asked to unset that the rule didn't have.

      $ stylebot css rule 1234 '.subtext' --set color=#a8a8a8
      Saved news.ycombinator.com
      Checked on tab 1234.
      Elements each added or changed rule matched:
      - .subtext: 30 elements
      5 unchanged rules not listed
      
    • css diff prints each rule added or removed whole, and for each rule changed only the declarations that differ:

      $ stylebot css diff news.ycombinator.com --file hn.css
      --- news.ycombinator.com, as saved
      +++ the css given
      
        .subtext {
      -   color: #828282;
      +   color: #a8a8a8;
        }
      
      + .comment (in @media (max-width: 750px)) {
      +   font-size: 15px;
      + }
      
      Rules: 1 added, 1 changed, 0 removed. Nothing saved.
      

Profiles

stylebot profile create Dark news.ycombinator.com --use
stylebot css set news.ycombinator.com --profile Dark < dark.css
  • profiles lists a style's profiles, starring the one in use.
  • profile create, use, rename and delete manage them.
  • css get, rule, add, set and diff take --profile to name the profile rather than use the active one. The /stylebot skill always passes it, so a profile switch mid-session can't send its css into your own style.

Style options

stylebot options news.ycombinator.com
stylebot option set scheme dark news.ycombinator.com
  • options lists the options a profile's css declares (see Style options), each with its type, its label when the author gave one, its value, the values it takes and its default. A checkbox takes 0 and 1.
  • option set sets one, applying it to open tabs as the popup does. It refuses an option the profile doesn't declare, or a value it doesn't take.
  • Both take --profile to name the profile rather than use the active one. The /stylebot skill uses option set to check a light/dark style in each variant, then sets it back to the user's choice.

Output

Every command takes --json to print the raw response instead of text.

CLI mode

CLI mode lets you work with your agent from the page. A small terminal on a connected tab shows each command the agent runs there and each save, with an undo. You mark areas of the page and write notes about them; the agent makes the change and answers in the terminal.

  • Connect: ask your agent to watch, then click [connect] on the tab you're looking at, or let the agent connect one with mode <tab>. One tab is connected at a time; others offer [move here]. The terminal's close button or mode <tab> --off disconnects it.
  • Mark: drag over part of the page. Click a mark's numbered badge to remove it.
  • Note: type in the terminal and press Enter (Shift+Enter for a new line). The agent gets the note with a screenshot of your marks, and shows as thinking until it answers.

Keys, anywhere on the page but a field: . open or minimize, / write a note, Z undo the last save, X clear the marks.

The agent follows along with watch, which prints a line for each event (the tab attaching, each mark and note, navigations, edits outside the CLI), or selection, which lists the notes and marks sent since it last looked, with the elements inside each mark; --wait waits for the next one. say answers your note. A session might go:

$ stylebot watch
waiting for a tab: ask the user to connect the tab to work on with the connect button Stylebot shows on it, or run: stylebot mode <tab>
attached tab 54432929 https://news.ycombinator.com/
mark ① area 640×48
note "keep this bar on screen when I scroll" about ① (screenshot /Users/you/.stylebot/work/stylebot-note-54432929-1.png)

$ stylebot css rule 54432929 '#hnmain > tbody > tr:first-child' --set position=sticky --set top=0 --profile Default
$ stylebot say 54432929 'The top bar now stays at the top as you scroll.'

Coding agents

The Stylebot skill teaches a coding agent to restyle pages with the CLI: open the page, read its outline, write CSS, fix what the page check reports, then look at a screenshot. It works in any agent with a shell and a file-writing tool, running on the same machine as the browser, once the setup above is done. Ask for a look, such as "give Hacker News a dark theme", and the agent picks the skill up on its own; or invoke it by name. Without saying what to change, it offers suggestions for the page.

The skill keeps its CSS and screenshots in ~/.stylebot/work/. When you name a profile, it updates that one, and in CLI mode the one you're using; otherwise its changes go into a new profile named after the agent and the look, such as "Claude: Dark", so switching back to your own style is one profile change.

Agents with plugin marketplaces install it from this repo's, as a plugin whose stylebot command runs the CLI on your PATH, or the published one through npx when there's none. The others copy the skill's folder, and need the CLI installed with npm install -g @stylebot/cli.

Agent How Checked
Claude Code This repo's marketplace Restyling measured by the styling evals
Codex CLI This repo's marketplace Installed and its skill listed, Codex 0.163.0
Copilot CLI This repo's marketplace Installed and its skill loaded, Copilot 1.0.99-2
Copilot (VS Code) Copy the skill From VS Code's docs only
Cursor Copy the skill From Cursor's docs only
Antigravity CLI Copy the skill From Antigravity's docs only

How well an agent restyles pages is only measured with Claude Code (see docs/styling-evals.md); the others were checked for installing the skill and finding it, by the author of #1223. Each agent's commands and folders below follow its own docs as of October 2026.

Claude Code

claude plugin marketplace add ankit/stylebot
claude plugin install stylebot

Use ./ in place of ankit/stylebot to install this checkout's plugin, or try it in one session with claude --plugin-dir tools/agent-plugin. Invoke it with /stylebot <site> <what to change>. It runs without permission prompts: the skill pre-approves stylebot commands and reading and writing files in ~/.stylebot/work/.

Codex CLI

codex plugin marketplace add ankit/stylebot
codex plugin add stylebot@stylebot

In a new session, ask for a look, or pick stylebot from /skills. To run stylebot commands without approving each one, choose to always allow them at the first prompt, or add a rule to ~/.codex/rules/default.rules:

prefix_rule(pattern = ["stylebot"], decision = "allow")

Copilot CLI

copilot plugin marketplace add ankit/stylebot
copilot plugin install stylebot@stylebot

copilot skill list shows whether the skill loaded. Copilot doesn't keep command approvals between sessions: approve stylebot for the rest of the session at the first prompt, or start it with copilot --allow-tool='shell(stylebot:*)'.

Copilot in VS Code, Cursor and Antigravity CLI

From this checkout, copy the skill folder into the agent's personal skills folder, then restart the agent:

Agent Personal skills folder Project skills folder
Copilot (VS Code) ~/.copilot/skills/ .github/skills/
Cursor ~/.cursor/skills/ .cursor/skills/
Antigravity CLI ~/.gemini/antigravity-cli/skills/ .agents/skills/
cp -R tools/agent-plugin/skills/stylebot ~/.cursor/skills/

Copilot CLI reads ~/.copilot/skills/ too, and Cursor also reads ~/.claude/skills/. Ask for a look, or type /stylebot.

To run stylebot commands without a prompt each time:

  • Copilot in VS Code: add "chat.tools.terminal.autoApprove": { "stylebot": true } to your user settings.
  • Cursor: pick Allowlist under Cursor Settings > Agents > Approvals & Execution, and add stylebot to the allowlist, or put { "terminalAllowlist": ["stylebot"] } in ~/.cursor/permissions.json, which replaces the allowlist in settings.
  • Antigravity CLI: choose to always allow stylebot at the first prompt, or add "permissions": { "allow": ["command(stylebot)"] } to ~/.gemini/antigravity-cli/settings.json.

A second browser

A test, or a second worktree, can run its own dev browser beside yours. It connects like any other browser, so the CLI asks which to use. To keep it apart entirely, give it its own profile and socket:

STYLEBOT_PROFILE_DIR=.chrome-dev-profile-test STYLEBOT_SOCKET=~/.stylebot/test.sock yarn start:chrome

Run stylebot install once the profile folder exists, so it's registered there too, and run the CLI with the same STYLEBOT_SOCKET to drive that browser. With it set, the CLI sees only that browser, and without it, never sees it.

To try the CLI against a release-like build, run yarn preview:chrome instead of yarn start:chrome. It carries the store's key, so it gets the extension id the host allows.

How it works

The browser starts the native host when the extension connects to it, and keeps it running while the connection is open. Each browser profile with Stylebot connected starts its own host. Each host listens on its own Unix socket in ~/.stylebot/browsers/, readable only by you, and relays each CLI command to the extension and its answer back.

The CLI finds the browsers through their sockets. With more than one, it asks each extension which browser it runs in and what page that browser shows, to name them for --browser and to ask you which to use. A socket nothing listens on, left by a host that quit without cleaning up, is removed. A host started before each browser had its own socket listens on the one all browsers used to share, and the CLI finds it there too until the browser restarts.

The code that reads a page loads into it the first time the CLI asks about that page, so pages the CLI never touches don't carry it. It runs in the top frame only.

The extension connects only while the setting is on and the browser has granted its optional nativeMessaging permission for the host. Screenshots need <all_urls>, which Stylebot already holds on Chrome and Edge, to restore open tabs after an update.

Chrome lets the background reach the native host only once it starts again after the grant, so turning the setting on restarts the extension once. A restart cuts off the content scripts already running in open tabs, and Chrome only injects into pages that load afterwards. So after the restart Stylebot runs its content scripts again in the open tabs, as it does after an update. The fresh copy removes the panel the old one left, which no longer responds.

Versions

After it's published, the extension updates from the browser's store and the CLI from npm, so they can drift apart. Each request carries the CLI's protocol number and version, and each response the extension's. They work together only when the protocol numbers are equal: the extension refuses a request in another protocol without running it, and the CLI says which side to update, the CLI with npm or Stylebot in the browser. An extension that sends no protocol predates the check, so the CLI asks for Stylebot to be updated. The check rides on every command, with no extra round trip.

Bump the protocol, in the CLI and the extension together, when a change to a command's arguments or result would break the other side: renaming or removing a command or a field, or changing what one means. Adding a command or an optional field doesn't need a bump.

The host copy in ~/.stylebot/ records the CLI version that installed it. When a different version of the CLI runs, it copies the host again first, and when the node the launcher is pinned to is gone, such as after a node upgrade, it pins the launcher to its own. Both happen quietly before the command runs. A browser keeps running the host it started until the extension reconnects, which is harmless while the host only relays; after a repin, though, the browser couldn't start the host at all, so the CLI asks for a browser restart if it then can't connect.

Windows

Windows has no Unix sockets, so there each host listens on a named pipe for your user, and a STYLEBOT_SOCKET path names a pipe after its file. A pipe can't be taken over, so with STYLEBOT_SOCKET set, the first browser to connect keeps it until it quits.

Where browsers find the host

Browsers find the host through a manifest that names its launcher and allows Stylebot's Chrome Web Store and Edge Add-ons ids. On macOS and Linux the manifest goes in a NativeMessagingHosts folder in the browser's user data folder, and the dev profiles get their own. On Windows one manifest in ~/.stylebot/ is registered under a NativeMessagingHosts key in HKCU\Software.

Chrome

  • macOS: ~/Library/Application Support/Google/Chrome
  • Linux: ~/.config/google-chrome
  • Windows: HKCU\Software\Google\Chrome

Edge

  • macOS: ~/Library/Application Support/Microsoft Edge
  • Linux: ~/.config/microsoft-edge
  • Windows: HKCU\Software\Microsoft\Edge

Brave

Brave reads Chrome's hosts on macOS and Windows, so it shares Chrome's registration there.

  • macOS: ~/Library/Application Support/Google/Chrome
  • Linux: ~/.config/BraveSoftware/Brave-Browser
  • Windows: HKCU\Software\Google\Chrome

Chromium

  • macOS: ~/Library/Application Support/Chromium
  • Linux: ~/.config/chromium
  • Windows: HKCU\Software\Chromium

Vivaldi

Vivaldi reads Chrome's key on Windows.

  • macOS: ~/Library/Application Support/Vivaldi
  • Linux: ~/.config/vivaldi
  • Windows: HKCU\Software\Google\Chrome

Arc

  • macOS: ~/Library/Application Support/Arc/User Data

Before it can ship

  • Publishing the CLI to npm, with the v4 store release, and the plugin through a public marketplace
  • Install support for Firefox
  • A visible sign that the CLI is connected, since any local process can use the socket