Commands
The whole surface, what each one prints, and what it exits with.
page-scanner pair [--port <n>] [--rotate] [--wait <s>=120] [--no-wait] [--json]
page-scanner status [--json]
page-scanner browsers [--json]
page-scanner tabs [--browser <id|label>] [--wait <s>=30] [--json]
page-scanner scan (--url <u> | --tab <id>) [--window <id>] [--browser <id|label>]
[--format pdf|png|jpeg=pdf] [--page-size auto|a4|letter=a4]
[--quality <0-1>] [--video frame|blank] [--open-editor]
[--out <file|dir>] [--wait <s>=30] [--timeout <s>=120] [--json]
page-scanner serve [--daemon] [--idle <min>]
page-scanner stop [--json]
page-scanner --version | --helpscan
Exactly one of --url and --tab. --url opens a background tab, captures it and closes it
again; --tab takes an id from tabs and never closes what you already had open.
--out is a file when it ends in a 2 to 5 character extension and is not an existing
directory, and a directory otherwise, in which case the browser's suggested filename is used
inside it. Parent directories are created. A relative path is relative to your working
directory, not the daemon's, which is why the daemon hands the bytes back and the command writes
them.
--page-size decides what a PDF is laid onto. a4 and letter slice the capture across
printable sheets with a half-inch margin; auto is one page the exact size of the capture, which
is not printable and which Acrobat clamps past 200 inches.
Waiting
Chrome retires the extension's service worker after about thirty seconds of silence, and it dials
back in when something wakes it. --wait covers that reconnect, and defaults to 30 seconds for
tabs and scan. --wait 0 means do not wait at all, and fails immediately instead.
What a command prints
- stdout carries the answer and nothing else. For
scanthat is one line, the absolute path. Fortabsandbrowsersit is a column-aligned table with no separator row, soawkcan read it. - stderr carries anything addressed to a person: progress, warnings, the reason something failed.
--jsonputs exactly one JSON document on stdout, for success and for failure alike, and leaves stderr empty.
{
"ok": true,
"path": "/Users/you/pdf.pdf",
"width": 1280,
"height": 4200,
"mode": "vector",
"selectableText": true,
"truncated": null,
"browserId": "b-9f2c41",
"fileName": "PDF - Wikipedia.pdf"
}{ "ok": false, "code": 3, "error": "NO_BROWSER", "message": "No browser is connected." }truncated is null rather than absent when the capture was whole, so a reader can see the
question was asked. When it is not null it carries what the page measured, what was captured, and
a sentence naming the gap.
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | the browser was reached and the work failed |
| 2 | the arguments were wrong |
| 3 | no usable browser: none connected, several connected, or the one named is not |
| 4 | not paired |
| 5 | the daemon would not start |
The daemon
Started on demand by the first command that needs one, and reused by every command after it, including the MCP server's.
page-scanner serve runs it in the foreground, which is how to see why it will not start.
page-scanner stop stops it. It exits on its own after 15 idle minutes
(PAGE_SCANNER_IDLE_MINUTES, 0 for never) and never while a browser is attached: the open
socket is what keeps Chrome's service worker alive.
Files
Everything lives in ~/.page-scanner, or in $PAGE_SCANNER_HOME if that is set: config.json
(the pairing), daemon.json (the running daemon) and daemon.log. The first two are mode 0600,
which Windows does not enforce: NTFS has no such mode, so both are readable by anything
running as you. On a shared machine, restrict the directory yourself.
icacls %USERPROFILE%\.page-scanner /inheritance:r /grant:r %USERNAME%:FFrom Node
import { scan, listTabs } from '@page-scanner/cli';
const { tabs } = await listTabs();
const result = await scan({ tabId: tabs[0].tabId, out: './out/', format: 'pdf' });
console.log(result.path, result.selectableText);