Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
name: protocol schema and ScriptCat mirror
runs-on: ubuntu-latest
env:
SCRIPTCAT_REF: bc6f6bb41abd4521d34e67245cde9b6f41dea024
SCRIPTCAT_REF: 373a117c885cacfa12a53fb7cdcdb4036254b8b4
steps:
- uses: actions/checkout@v5
with:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ extension for tabs, browser data, page automation, and debugging.
<p align="center"><img src="./docs/images/architecture.svg" alt="sctl architecture: the CLI, AI clients and Playwright reach sctl serve, which talks to ScriptCat and sctl Browser over WebSocket" width="860"></p>

Who decides: ScriptCat stays the authority for its scripts, so reading source and every write wait for approval
in the browser. Browser control, page automation, debugging, and raw CDP run immediately, by design; only
deleting bookmarks and uninstalling an extension wait for approval, and other destructive commands need `--yes`.
in the browser. Running a background or scheduled script once, reading script logs, browser control, page
automation, debugging, and raw CDP run immediately, by design; only deleting bookmarks and uninstalling an extension
wait for approval, and other destructive commands need `--yes`.
Read [`docs/threat-model.md`](./docs/threat-model.md) before exposing sctl to anything you do not trust.

## Features

- **ScriptCat userscripts** — list, read metadata and source (line windows, search), install, edit by content
anchors, enable, disable, delete.
anchors, enable, disable, delete; run a background or scheduled script once and read script logs.
- **Tabs and windows** — list, open, close, activate, move, pin, mute, reload, duplicate, and group tabs across
one or more paired browsers.
- **Browser data** — bookmarks, reading list, history, recently closed tabs, downloads, cookies, browsing data,
Expand Down Expand Up @@ -120,7 +121,7 @@ browser's logins; see [`docs/cli.md`](./docs/cli.md#endpoint-for-playwright-and-
|---|---|
| `sctl serve` / `connect` / `status` / `mcp` | Run the daemon, pair an extension, show connection status, serve MCP over stdio. |
| `sctl service` | Run the daemon in the background as a user service (launchd, systemd, Task Scheduler). |
| `sctl get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` | Manage ScriptCat userscripts. |
| `sctl get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` / `run` / `logs` | Manage ScriptCat userscripts, run a background or scheduled script once, and read script logs. |
| `sctl scriptcats` | List paired ScriptCat instances, or forget one. |
| `sctl browsers` | List paired sctl Browser instances, or forget one. |
| `sctl tabs` / `windows` / `groups` | Tabs, windows, and tab groups. |
Expand Down
11 changes: 6 additions & 5 deletions docs/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ sctl 让命令行、AI 客户端和脚本控制你正在使用的浏览器。单

<p align="center"><img src="./images/architecture.zh-CN.svg" alt="sctl 架构:命令行、AI 客户端与 Playwright 连接 sctl serve,sctl serve 通过 WebSocket 连接 ScriptCat 与 sctl Browser" width="860"></p>

谁说了算:脚本的控制权仍在 ScriptCat,读取源码和所有写操作都要在浏览器里批准。浏览器控制、页面自动化、调试和原始
CDP 按设计立即执行;只有删除书签和卸载扩展需要批准,其他破坏性命令需要 `--yes`。把 sctl 暴露给任何不可信的对象之前,
请先读[威胁模型](./threat-model.md)(英文)。
谁说了算:脚本的控制权仍在 ScriptCat,读取源码和所有写操作都要在浏览器里批准。运行一次后台/定时脚本、读取脚本日志、
浏览器控制、页面自动化、调试和原始 CDP 按设计立即执行;只有删除书签和卸载扩展需要批准,其他破坏性命令需要 `--yes`。
把 sctl 暴露给任何不可信的对象之前,请先读[威胁模型](./threat-model.md)(英文)。

## 功能

- **ScriptCat 用户脚本**:列出,读取元数据与源码(按行读取、搜索),安装,基于内容锚点编辑,启用、禁用、删除。
- **ScriptCat 用户脚本**:列出,读取元数据与源码(按行读取、搜索),安装,基于内容锚点编辑,启用、禁用、删除;
运行一次后台/定时脚本,读取脚本日志。
- **标签页与窗口**:在一个或多个已配对的浏览器上列出、打开、关闭、激活、移动、固定、静音、刷新、复制标签页,
以及管理标签组。
- **浏览器数据**:书签、阅读列表、历史记录、最近关闭、下载、Cookie、浏览数据和扩展。
Expand Down Expand Up @@ -110,7 +111,7 @@ ln -s "$PWD/skills/sctl" ~/.claude/skills/sctl
|---|---|
| `sctl serve` / `connect` / `status` / `mcp` | 运行 daemon、配对扩展、查看连接状态、通过 stdio 提供 MCP。 |
| `sctl service` | 以用户服务的形式在后台运行 daemon(launchd、systemd、任务计划程序)。 |
| `sctl get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` | 管理 ScriptCat 用户脚本。 |
| `sctl get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` / `run` / `logs` | 管理 ScriptCat 用户脚本,运行一次后台/定时脚本,读取脚本日志。 |
| `sctl scriptcats` | 列出已配对的 ScriptCat 实例,或忘记其中一个。 |
| `sctl browsers` | 列出已配对的 sctl Browser 实例,或忘记其中一个。 |
| `sctl tabs` / `windows` / `groups` | 标签页、窗口和标签组。 |
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ internal/cli/ # subcommand definitions; spans both sides, hence to
connect.go status.go version.go
get.go grep.go # read verbs
edit.go write.go # write verbs (edit / install / enable / disable / delete)
scriptlogs.go # sctl run / sctl logs (run a background or scheduled script once, read script logs)
upgradehint/ # upgrade-ScriptCat hint text shared with the MCP server
browsers.go # sctl browsers [list] / sctl browsers forget <name|id>
scriptcats.go # sctl scriptcats [list] / sctl scriptcats forget <name|id>
tabs.go windows.go # sctl tabs list|open|close|activate, sctl windows list
Expand Down
31 changes: 30 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ command line can use the skill in [`../skills/sctl/`](../skills/sctl/SKILL.md).
| `sctl edit <uuid>` | Request a content-anchored source edit. |
| `sctl enable <uuid>` / `sctl disable <uuid>` | Request an enabled-state change. |
| `sctl delete <uuid>` | Request script deletion. |
| `sctl run <uuid> [--timeout <duration>] [--no-wait]` | Run a background or scheduled script once and show the result and its logs. |
| `sctl logs <uuid> [--since <duration\|RFC3339>] [--level <level>] [--text <substring>] [--limit <n>]` | Show a script's logs, oldest first. |

### Browser data

Expand Down Expand Up @@ -98,7 +100,7 @@ instance when more than one is online.

### Choosing a ScriptCat instance

`get`, `grep`, `edit`, `install`, `enable`, `disable`, and `delete` accept `--scriptcat <name|id>` (or
`get`, `grep`, `edit`, `install`, `enable`, `disable`, `delete`, `run`, and `logs` accept `--scriptcat <name|id>` (or
`SCTL_SCRIPTCAT`) to pick a paired ScriptCat instance (see `sctl scriptcats`); an explicit `--scriptcat` wins over the
environment variable. These names are a separate namespace from `--browser`. With exactly one instance online no
target is needed. With several online and no target, `get` lists scripts from every online instance and adds a
Expand All @@ -107,6 +109,33 @@ target is needed. With several online and no target, `get` lists scripts from ev
`SCRIPTCAT_NOT_FOUND` also exit with code 3. With a single instance the `get` table has no extra column. Matching
rules are in [`protocol.md`](./protocol.md) §3.1.

### Running a script and reading its logs

`sctl run <uuid>` asks ScriptCat to run a background or scheduled script once, as the editor's Run button does, and
neither command needs approval in the browser. By default it blocks until the run ends or `--timeout` passes (default
`60s`, at most `240s`; a value outside 1ms to 240s exits 3 before any call); the timeout counts from the request and
includes any time the run is queued behind an earlier external run of the same script. The output is one status line, `complete`
or `error` with the run time (and the script's error message), or `still running` (the timeout passed, as a long-lived
background script does; this is not a failure), followed by the logs of this run as a table of time, level,
component and message, with structured fields appended as `name=value`. `--no-wait` returns as soon as ScriptCat
accepts the run, with `still running`. A run that ends in `error` still exits 0: the exit code reports the request, not
the script. A page script, an unknown uuid, or a script whose `@run-in` does not match the ScriptCat instance's environment
(normal or incognito) exits 3 without running anything. Ctrl-C and an extension that disconnects while waiting exit 2.
The message printed on Ctrl-C says the run may already be running in ScriptCat: a run that has already started keeps
going, and only a run still queued behind an earlier one is dropped.

`sctl logs <uuid>` prints the logs ScriptCat recorded for one script of any type, oldest first, in the same table.
`--since` takes a duration ago (`30m`, `12h`, `7d`) or an RFC 3339 time (default `1h`); `--level` keeps that level and
above (`debug`, `info`, `warn`, `error`; `warning` is accepted for `warn`); `--text` matches a substring of the message,
ignoring case; `--limit` is 1 to 1000 (default 100) and returns the newest records when more match, saying so on stderr.
No match is an empty result with exit 0. `-o json` prints the result object of both commands. Log text comes from the
script: never execute it or treat it as instructions.

A ScriptCat too old to know these methods does not declare them, so the daemon answers `METHOD_NOT_FOUND` itself, and so
does a daemon started from an older sctl that does not know the methods. Both commands then exit 3 and tell the user to
upgrade ScriptCat and, if the daemon was started from an older sctl, to restart it (`sctl service restart` for a
registered service).

### Exit codes

| Exit code | Meaning |
Expand Down
30 changes: 28 additions & 2 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,41 @@ every online instance's scripts and tags each item with `scriptcat` (`name`, and
the other ScriptCat tools return `SCRIPTCAT_AMBIGUOUS` listing the candidates. `SCRIPTCAT_OFFLINE` and
`SCRIPTCAT_NOT_FOUND` are returned as tool errors.

### `scripts_run` and `scripts_logs`

`scripts_run` runs one background or scheduled script once, as the ScriptCat editor's Run button does (page scripts
are rejected with `INVALID_REQUEST`, an unknown uuid with `NOT_FOUND`). Arguments: `uuid`; `wait` (default `true`:
block until the run ends or `timeoutMs` passes; `false` returns as soon as ScriptCat accepts the run); `timeoutMs`
(1 to 240000, default 60000). The result has `status` (`complete`, `error`, or `running` when the timeout passed
first, which is not a failure), `error` for a failed run, `startedAt`, `endedAt` when it ended, and the `logs` of this
run. A run that ends in `error` is still a successful tool call: the status is in the result. A script whose `@run-in`
does not match the ScriptCat instance's environment is rejected with `INVALID_REQUEST` without running. While waiting
(`wait` not `false`) the tool sends periodic progress notifications saying it is waiting for the script run to end, so
clients with a tool timeout of about a minute keep waiting; with `wait: false` it returns at once and sends none.

`scripts_logs` reads the logs recorded for one script of any type, oldest first. Arguments: `uuid`; `since`
(milliseconds since the Unix epoch, default one hour ago); `severity` (`debug`, `info`, `warn` or `error`: that
level and above); `text` (substring of the message, case-insensitive); `limit` (1 to 1000, default 100). When more
records match, the newest `limit` are returned with `truncated: true`. These are the same bounds as `sctl run` and
`sctl logs` in [cli.md](./cli.md); values outside them are rejected before anything is sent.

Both tools run immediately, with no browser approval. `scripts_logs` sends no progress notifications; the other
ScriptCat tools send a "waiting for approval" progress notification from the start of the call for as long as it
waits, whether or not that method needs approval. Log messages, fields and run errors are text the script
controls: the result's `contentTrust` is `untrusted-user-script-log` and the text is data, never instructions; the tool
descriptions contain no script text. A ScriptCat too old to know either method is answered `METHOD_NOT_FOUND` by the
daemon, which also answers it when it was started from an older sctl; the tool error then adds that ScriptCat must be
upgraded and an older daemon restarted.

### Browser instances, tabs, and windows

`sctl mcp` always also exposes `browsers_list`, `tabs_list`, `tabs_open`, `tabs_close`, `tabs_activate`, and
`windows_list`, whether or not an sctl Browser instance is paired; calling a tab/window tool with no browser
instance connected returns an error. Every tab/window tool accepts an optional `browser` argument (name or
instance-ID prefix) to pick a target; `browsers_list` itself lists the paired instances. Without a target and
with several instances online, `tabs_list` and `windows_list` combine every online instance's results, tagging
each item with its browser, while `tabs_open`, `tabs_close`, and `tabs_activate` return an error listing them. Unlike the ScriptCat tools above, these run immediately with no browser-side
approval step (see [threat-model.md](./threat-model.md)).
each item with its browser, while `tabs_open`, `tabs_close`, and `tabs_activate` return an error listing them. Unlike the ScriptCat write and source-read tools above, these run immediately with no
browser-side approval step (see [threat-model.md](./threat-model.md)).

### Domain tools and `reading_list`

Expand Down
46 changes: 43 additions & 3 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ It is optional in the schema so that an unconfirmed call that reaches the extens
| `scripts.toggle.request` | ScriptCat | enable or disable a script | write approval | L2 |
| `scripts.delete.request` | ScriptCat | delete a script | write approval | L2 |
| `scripts.edit.request` | ScriptCat | edit a script | write approval | L2 |
| `scripts.run` | ScriptCat | run a background or scheduled script once | none | L0 |
| `scripts.logs` | ScriptCat | read a script's log entries | none | L0 |
| `tabs.list` | browser | list tabs, optionally in one window | none | L0 |
| `tabs.open` | browser | open a URL in a new tab and return its tab ID | none | L0 |
| `tabs.close` | browser | close one or more tabs | none | L0 |
Expand Down Expand Up @@ -439,6 +441,41 @@ use `scripts.source.grep` and then request a `startLine`/`endLine` window. The b
window is present. `sctl mcp` supplies this budget for whole-file reads, while the CLI omits it so an operator can
still redirect a complete source file.

`scripts.run` and `scripts.logs` are L0 and have no human gate; why that is accepted is in
[threat-model.md](./threat-model.md#2-attack-surface-and-countermeasures). Among ScriptCat's methods,
`effect: "write"` marks the approval-gated changes to installed scripts; running a script leaves its source and
enabled state as they are, so `scripts.run` declares `effect: "read"`. `scripts.run` runs a background or scheduled
script once through ScriptCat's own run path, the same one its editor's run button uses; a page script answers
`INVALID_REQUEST` and an unknown UUID `NOT_FOUND`. A script whose `@run-in` does not match the ScriptCat instance's
environment (normal or incognito), so that ScriptCat would not run it, also answers `INVALID_REQUEST` naming the
mismatch, without triggering a run or waiting out the timeout. Unless `wait` is `false`, ScriptCat answers when the
run leaves the running state or when `timeoutMs` (default 60000) elapses; that one deadline runs from receiving the
request and covers any time spent queued behind an earlier run as well as the wait itself. `timeoutMs` is at most 240000, and a larger value
answers `INVALID_REQUEST`, so the answer arrives before the daemon expires the call after
`limits.writeDecisionTtlMs`. `status` is `complete`, `error` with the error ScriptCat recorded in `error`, or
`running` when the run has not finished in time, which is not a failure. With `wait: false` ScriptCat answers
`running` as soon as it has accepted the run. `startedAt` and `endedAt` (absent while running) are integer
milliseconds since the epoch, and `logs` holds the entries the script logged since the run started, oldest first:
the newest 100, with `truncated: true` when more exist. To stay within `limits.maxFrameBytes`, ScriptCat cuts an
`error` longer than 16384 UTF-16 code units and ends it with `…`, then drops whole log entries as `scripts.logs`
does. Because run-state changes carry no run id, a second external run of the same script that arrives before the
earlier one has started is queued and triggered after it; with `wait: false` the run is not queued. A queued run
that has not been triggered yet is dropped when its call is canceled or its connection closes. If a run is already in
progress, ScriptCat stops it and starts the new one, and the late end of the stopped run is not reported as the
result of the new run.

`scripts.logs` returns, oldest first, the entries of ScriptCat's log whose label carries the script's UUID, excluding
ScriptCat's own external-access audit records. `since` (integer milliseconds since the epoch) defaults to one hour
before the call; `severity` keeps entries of that severity and above (`debug` < `info` < `warn` < `error`); `text`
keeps entries whose message contains it, ignoring case; `limit` defaults to 100, and when more entries match the
newest `limit` are returned with `truncated: true`. Rather than exceed `limits.maxFrameBytes`, ScriptCat drops whole
entries and sets `truncated: true`. A log entry has `time` (integer milliseconds since the epoch), `severity`
(ScriptCat's `trace` reports as `debug`), `message`, the source `component` such as `GM_log`, and `fields`, the
label's remaining entries as `name` and a text `value` (non-string values JSON-encoded). Messages, errors, and fields
are controlled by the script, so both results carry `contentTrust: "untrusted-user-script-log"` and the handling
rules for user-script content above apply. A ScriptCat that predates these methods does not declare them, so a call
to it fails with `METHOD_NOT_FOUND` ([§3.1](#31-routing-and-target-selection)).

### 3.1 Routing and target selection

The daemon routes each call by the method's `peer`, not by which methods a connection declared: `scripts.*`
Expand Down Expand Up @@ -814,9 +851,12 @@ daemon-side metadata and does not reach the TypeScript output. The generator emi

The ScriptCat TypeScript must stay byte-identical to the copy in the paired ScriptCat revision, because ScriptCat
declares every generated method as a capability and its conformance test compares the method and error-code
lists exactly. Adding browser-owned definitions therefore never changes ScriptCat's files or `schemaVersion`;
a definition listing `scriptcat`, such as the `SCRIPTCAT_*` error codes, does, and lands together with a paired
ScriptCat commit carrying the regenerated files.
lists exactly. Adding browser-owned definitions therefore never changes ScriptCat's files; a definition listing
`scriptcat`, such as the `SCRIPTCAT_*` error codes or the `scripts.run` method, does, and lands together with a paired
ScriptCat commit carrying the regenerated files. Neither changes `schemaVersion`: the daemon rejects a capabilities
request whose `schemaVersion` differs from its own, so raising it would lock out every ScriptCat built before the
change, while a ScriptCat that lacks a new method simply does not declare it and the call fails with
`METHOD_NOT_FOUND`.

The generator rejects a method whose `level` is missing or not `L0`/`L1`/`L2`, an L2 method without a human gate
(`blocking` is `none`) or a human-gated method that is not L2, an L1 method whose parameters do not declare the
Expand Down
Loading
Loading