Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2cfc65e
docs: spec for sctl browser foundation
CodFrm Sep 28, 2026
8c6b419
docs: generate protocol bindings per peer in browser foundation spec
CodFrm Sep 28, 2026
90e429a
feat: annotate protocol ownership and generate bindings per peer
CodFrm Sep 28, 2026
0fe1702
feat: hold ScriptCat and several browser instances in the daemon
CodFrm Sep 28, 2026
e8f2e7c
feat: route calls by method ownership and resolve browser targets
CodFrm Sep 28, 2026
4c6a147
feat: add sctl browsers commands and browser status
CodFrm Sep 28, 2026
95a6020
feat: add sctl Browser extension project and connection core
CodFrm Sep 28, 2026
c7cf7d7
feat: expose browser tools over MCP
CodFrm Sep 28, 2026
e32fcbe
test: verify MCP tool descriptions are static and never spliced with …
CodFrm Sep 28, 2026
f1af2ab
feat: add sctl tabs and windows commands
CodFrm Sep 28, 2026
5b0c5f7
feat: handle tab and window methods in the extension
CodFrm Sep 28, 2026
735accf
feat: add the sctl Browser popup
CodFrm Sep 28, 2026
241d6d5
docs: cover the sctl Browser extension across architecture, AGENTS, a…
CodFrm Sep 28, 2026
fafe52c
fix: align sctl Browser foundation with its spec
CodFrm Sep 28, 2026
2b4dd1a
docs: separate brand and text colours in the browser popup palette
CodFrm Sep 28, 2026
a2fb0ea
fix: split brand and primary colours in the browser popup light palette
CodFrm Sep 28, 2026
a206ee2
fix: keep browser popup hover and invalid states on the spec palette
CodFrm Sep 28, 2026
db9fcb1
fix: harden browser foundation edge paths found in code review
CodFrm Sep 28, 2026
1230a89
test: cancel the MCP call only after it reaches the bridge
CodFrm Sep 28, 2026
56b87ad
fix: answer a reconnecting peer without waiting for its old connectio…
CodFrm Sep 28, 2026
78d27cd
fix: number popup reconnect retries correctly and correct pairing-cod…
CodFrm Sep 28, 2026
867d48d
fix: keep forget and repeated responses off the daemon's blocking paths
CodFrm Sep 28, 2026
b6265e3
fix: start popup re-pairing with an empty code and restore its draft …
CodFrm Sep 29, 2026
4afdbd2
fix: drop unused popup dictionary entries
CodFrm Sep 29, 2026
7074c44
fix: consume pairing codes atomically and harden popup, socket and MC…
CodFrm Sep 29, 2026
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
35 changes: 34 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,42 @@ jobs:
path: dist/*.${{ matrix.format }}
if-no-files-found: error

extension:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
package_json_file: extension/package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: extension/pnpm-lock.yaml
- name: 构建并打包浏览器扩展
working-directory: extension
run: |
set -euo pipefail
# 扩展版本号与 sctl 发布版本一致;预发布后缀由构建写进 manifest 的 version_name。
VERSION="${GITHUB_REF_NAME#v}"
COMMIT_TIMESTAMP=$(git show -s --format=%ct HEAD)
PACKAGE="sctl-browser-extension-${VERSION}"
pnpm install --frozen-lockfile
SCTL_EXTENSION_VERSION="$VERSION" pnpm build
mkdir -p ../dist
cp -R dist "../dist/${PACKAGE}"
find "../dist/${PACKAGE}" -exec touch -d "@${COMMIT_TIMESTAMP}" {} +
(cd ../dist && find "$PACKAGE" | sort | zip -X -q "${PACKAGE}.zip" -@)
- uses: actions/upload-artifact@v7
with:
name: release-extension
path: dist/*.zip
if-no-files-found: error

release:
name: Create Release
needs: build
needs: [build, extension]
runs-on: ubuntu-latest
permissions:
contents: write # 创建 GitHub Release 并上传产物
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ jobs:
- run: go vet ./...
- run: go test -race ./...

extension:
name: extension
runs-on: ubuntu-latest
defaults:
run:
working-directory: extension
steps:
- uses: actions/checkout@v5
# pnpm 版本取自 extension/package.json 的 packageManager 字段。
- uses: pnpm/action-setup@v4
with:
package_json_file: extension/package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: extension/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm format:check
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build

protocol-schema:
name: protocol schema and ScriptCat mirror
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ dist/
.dev-kit
# 一次性端到端验证脚本与证据(见 docs/verification.md),永远不进版本库
/e2e/scratch/
node_modules/
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ framework and [cobra](https://github.com/spf13/cobra).

```text
sctl mcp / CLI verbs ──/control/* HTTP──▶ sctl serve (daemon) ──WS──▶ ScriptCat extension (approval authority)
└─WS──▶ sctl Browser extension (1+ paired instances)
internal/client/ internal/daemon/ internal/pkg/ (shared by both sides)
```

The authority always lives on the extension side: the daemon approves no write on its own — it forwards the
request and blocks until a human decides in the browser. Full process model and package responsibilities are
The authority always lives on the extension side for ScriptCat's write and source-disclosure gates: the daemon
approves no write and discloses no source on its own — it forwards the request and blocks until a human
decides in the browser. Browser control (`sctl browsers`/`tabs`/`windows`) is the deliberate exception: it has
no human gate by design, so any control-token holder can drive a paired `sctl Browser` instance immediately —
see [`docs/threat-model.md`](./docs/threat-model.md). Full process model and package responsibilities are
in [`docs/architecture.md`](./docs/architecture.md).

## Engineering Principles
Expand Down
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ BUILD_DIR ?= bin
BINARY := $(BUILD_DIR)/sctl
VERSION_PACKAGE := github.com/scriptscat/sctl/internal/cli
SCRIPTCAT_DIR ?= ../scriptcat
BROWSER_PROTOCOL_DIR := extension/src/protocol/generated
PROTOCOL_GENERATED_DIRS := internal/pkg/protocol/generated $(BROWSER_PROTOCOL_DIR)

.PHONY: help build test lint dev protocol-generate protocol-sync-scriptcat protocol-check clean

Expand All @@ -28,8 +30,8 @@ dev: ## 构建并启动本地 daemon(DEV_VERSION=0.1.0)
$(GO) build -ldflags "-X $(VERSION_PACKAGE).Version=$(DEV_VERSION)" -o $(BINARY) ./cmd/sctl
$(BINARY) serve

protocol-generate: ## 从权威 schema 生成 Go、TypeScript 与 TypeScript 校验器
$(GO) run ./cmd/protocolgen -schema internal/pkg/protocol -out internal/pkg/protocol/generated
protocol-generate: ## 从权威 schema 生成 Go、ScriptCat 与浏览器扩展的 TypeScript 及校验器
$(GO) run ./cmd/protocolgen -schema internal/pkg/protocol -out internal/pkg/protocol/generated -browser-out $(BROWSER_PROTOCOL_DIR)

protocol-sync-scriptcat: protocol-generate ## 更新相邻 ScriptCat 仓库的生成产物
mkdir -p $(SCRIPTCAT_DIR)/src/app/service/service_worker/external_access/generated
Expand All @@ -40,7 +42,9 @@ protocol-sync-scriptcat: protocol-generate ## 更新相邻 ScriptCat 仓库的
$(SCRIPTCAT_DIR)/src/app/service/service_worker/external_access/generated/validators.generated.ts

protocol-check: protocol-generate ## 检查生成物已提交且可复现
git diff --exit-code -- internal/pkg/protocol/generated
git diff --exit-code -- $(PROTOCOL_GENERATED_DIRS)
@untracked="$$(git ls-files --others --exclude-standard -- $(PROTOCOL_GENERATED_DIRS))"; \
if [ -n "$$untracked" ]; then echo "未提交的生成物:"; echo "$$untracked"; exit 1; fi

clean: ## 删除本地构建产物
rm -rf $(BUILD_DIR)
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
[English](./README.md) | [简体中文](./docs/README_zh-CN.md)

sctl connects AI clients and command-line workflows to the
[ScriptCat](https://github.com/scriptscat/scriptcat) browser extension. One cross-platform binary provides a
local bridge daemon, a stdio MCP server, and script-management commands.
[ScriptCat](https://github.com/scriptscat/scriptcat) browser extension and to its own **sctl Browser** browser
extension. One cross-platform binary provides a local bridge daemon, a stdio MCP server, and script-management
and browser-control commands.

```text
AI client ── stdio MCP ──▶ sctl mcp ── local control API ──▶ sctl serve ── WebSocket ──▶ ScriptCat
Expand All @@ -16,9 +17,10 @@ confirmation UI in the extension.

## Features

- Exposes ScriptCat operations as discoverable, schema-typed MCP tools.
- Exposes ScriptCat operations and browser tab/window control as discoverable, schema-typed MCP tools.
- Lists scripts and reads metadata or source, including line windows and source search.
- Requests installation, content-anchored editing, enable/disable, and deletion through browser approval.
- Lists, opens, closes, and activates tabs and lists windows across one or more paired sctl Browser instances.
- Uses JSON-RPC 2.0 over a WebSocket with mutual authentication; the listener defaults to loopback.
- Ships as one binary; no browser automation or Native Messaging host is required.

Expand Down Expand Up @@ -60,6 +62,14 @@ sctl status
```

Enable **External Access** in ScriptCat and enter the one-time code printed by `connect`.

To also pair the **sctl Browser** extension (tab/window control), download
`sctl-browser-extension-<version>.zip` from [GitHub Releases](https://github.com/scriptscat/sctl/releases),
unzip it, and load the unzipped folder as an unpacked extension from your browser's extensions page. Open its
popup and enter a one-time code from `sctl connect`; a code pairs only one extension, so run `connect` again if
ScriptCat already used it. Full steps, including the browser's "developer mode" toggle, are in
[`docs/mcp.md`](./docs/mcp.md#4-enroll-scriptcat-and-sctl-browser).

Then configure the AI client to launch:

```text
Expand All @@ -76,18 +86,23 @@ troubleshooting.
| Command | Purpose |
|---|---|
| `sctl serve` | Run the local bridge daemon. |
| `sctl connect` | Open a one-time ScriptCat enrollment window. |
| `sctl mcp [--name <label>]` | Serve ScriptCat tools over stdio MCP. |
| `sctl connect` | Open a one-time enrollment window for ScriptCat or sctl Browser. |
| `sctl mcp [--name <label>]` | Serve ScriptCat and sctl Browser tools over stdio MCP. |
| `sctl status` | Show daemon and extension connection status. |
| `sctl get [<uuid>]` | List scripts or read one script. |
| `sctl grep <uuid> <query>` | Search one script's source. |
| `sctl install <url\|file>` | Request script installation. |
| `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 browsers [list]` / `sctl browsers forget <name\|id>` | List paired sctl Browser instances, or forget one. |
| `sctl tabs list\|open\|close\|activate` | List, open, close, or activate tabs on a paired sctl Browser instance. |
| `sctl windows list` | List windows on a paired sctl Browser instance. |

Run `sctl --help` or `sctl <command> --help` for usage and flags. Write operations block
until the user approves, rejects, or closes the confirmation flow in ScriptCat.
until the user approves, rejects, or closes the confirmation flow in ScriptCat; browser control commands run
immediately with no approval step (see [`docs/threat-model.md`](./docs/threat-model.md)). `tabs` and `windows`
accept `--browser <name|id>` (or `SCTL_BROWSER`) to pick an instance when more than one is online.

## License

Expand Down
5 changes: 3 additions & 2 deletions cmd/protocolgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (

func main() {
schema := flag.String("schema", "internal/pkg/protocol", "protocol source directory")
out := flag.String("out", "internal/pkg/protocol/generated", "generated output directory")
out := flag.String("out", "internal/pkg/protocol/generated", "output directory for the Go bindings and the ScriptCat TypeScript")
browserOut := flag.String("browser-out", "extension/src/protocol/generated", "output directory for the browser extension TypeScript")
flag.Parse()
if err := protocolgen.Generate(*schema, *out); err != nil {
if err := protocolgen.Generate(*schema, *out, *browserOut); err != nil {
fail(err)
}
}
Expand Down
28 changes: 20 additions & 8 deletions docs/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
[English](../README.md) | [简体中文](./README_zh-CN.md)

sctl 用于将 AI 客户端和命令行工作流连接到
[ScriptCat](https://github.com/scriptscat/scriptcat) 浏览器扩展。单个跨平台二进制同时提供本地桥接
daemon、stdio MCP Server 和脚本管理命令。
[ScriptCat](https://github.com/scriptscat/scriptcat) 浏览器扩展,以及它自己的 **sctl Browser**
浏览器扩展。单个跨平台二进制同时提供本地桥接 daemon、stdio MCP Server、脚本管理命令和浏览器控制命令。

```text
AI 客户端 ── stdio MCP ──▶ sctl mcp ── 本地控制 API ──▶ sctl serve ── WebSocket ──▶ ScriptCat
Expand All @@ -15,9 +15,10 @@ CLI ─────────────────────────

## 功能

- 将 ScriptCat 操作暴露为可发现、具有 Schema 类型的 MCP 工具。
- 将 ScriptCat 操作与浏览器标签页/窗口控制暴露为可发现、具有 Schema 类型的 MCP 工具。
- 列出脚本并读取元数据或源码,支持按行读取和源码搜索。
- 通过浏览器确认请求安装、基于内容锚点的编辑、启用/禁用和删除。
- 在一个或多个已配对的 sctl Browser 实例上列出、打开、关闭、激活标签页,以及列出窗口。
- 在仅监听回环地址的 WebSocket 上使用 JSON-RPC 2.0 和双向认证。
- 单二进制交付,不依赖浏览器自动化或 Native Messaging Host。

Expand Down Expand Up @@ -56,8 +57,14 @@ sctl connect
sctl status
```

在 ScriptCat 中启用**外部接入**,然后输入 `connect` 打印的一次性配对码。随后将 AI
客户端配置为启动:
在 ScriptCat 中启用**外部接入**,然后输入 `connect` 打印的一次性配对码。

若还要配对 **sctl Browser** 扩展(标签页/窗口控制),从 [GitHub Releases](https://github.com/scriptscat/sctl/releases)
下载 `sctl-browser-extension-<version>.zip` 并解压,在浏览器的扩展管理页把解压后的目录作为"已解压的扩展程序"加载,
打开其弹窗并输入 `sctl connect` 打印的一次性配对码;一个码只能配对一个扩展,若已被 ScriptCat 用掉,就再运行一次
`connect`。完整步骤(含浏览器的"开发者模式"开关)见[`mcp.md`](./mcp.md#4-enroll-scriptcat-and-sctl-browser)(英文)。

随后将 AI 客户端配置为启动:

```text
/absolute/path/to/sctl mcp --name my-ai-client
Expand All @@ -72,18 +79,23 @@ sctl status
| 命令 | 用途 |
|---|---|
| `sctl serve` | 运行本地桥接 daemon。 |
| `sctl connect` | 打开一次性 ScriptCat 接入窗口。 |
| `sctl mcp [--name <label>]` | 通过 stdio MCP 提供 ScriptCat 工具。 |
| `sctl connect` | 打开一次性接入窗口,供 ScriptCat 或 sctl Browser 配对。 |
| `sctl mcp [--name <label>]` | 通过 stdio MCP 提供 ScriptCat 与 sctl Browser 工具。 |
| `sctl status` | 查看 daemon 和扩展的连接状态。 |
| `sctl get [<uuid>]` | 列出脚本或读取单个脚本。 |
| `sctl grep <uuid> <query>` | 搜索单个脚本的源码。 |
| `sctl install <url\|file>` | 请求安装脚本。 |
| `sctl edit <uuid>` | 请求基于内容锚点编辑源码。 |
| `sctl enable <uuid>` / `sctl disable <uuid>` | 请求修改启用状态。 |
| `sctl delete <uuid>` | 请求删除脚本。 |
| `sctl browsers [list]` / `sctl browsers forget <name\|id>` | 列出已配对的 sctl Browser 实例,或忘记其中一个。 |
| `sctl tabs list\|open\|close\|activate` | 在已配对的 sctl Browser 实例上列出、打开、关闭或激活标签页。 |
| `sctl windows list` | 列出已配对的 sctl Browser 实例上的窗口。 |

运行 `sctl --help` 或 `sctl <command> --help` 查看用法和参数。写操作会阻塞,直到用户在
ScriptCat 中批准、拒绝或关闭确认流程。
ScriptCat 中批准、拒绝或关闭确认流程;浏览器控制命令按设计没有审批步骤、立即执行(参见
[`threat-model.md`](./threat-model.md))。当多个实例同时在线时,`tabs` 与 `windows` 可用
`--browser <name|id>`(或环境变量 `SCTL_BROWSER`)指定目标实例。

## 许可证

Expand Down
33 changes: 26 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@

```text
MCP client (Claude/Codex…) ─ stdio ─→ sctl mcp ─┐ (authenticated control API)
CLI verbs (sctl get / edit / install …)─────────┤
CLI verbs (sctl get / edit / install / browsers / tabs / windows …) ┤
▼
sctl serve (daemon; defaults to 127.0.0.1:8643)
▲ WebSocket (extension dials in + mutual HMAC handshake)
ScriptCat browser extension (authority for approval and authorization)
▲ WebSocket (each extension dials in + mutual HMAC handshake)
ScriptCat browser extension (authority for write approval and source disclosure)
sctl Browser extension, one or more paired instances (tab/window control)
```

`sctl mcp` and the CLI verbs are **separate processes** from `sctl serve`. They talk over the
`/control/*` HTTP/JSON control API on the daemon's listener — same port as the extension's WS surface, separate
`/control/*` HTTP/JSON control API on the daemon's listener — same port as the extensions' WS surface, separate
path, authenticated with the control token described in [threat-model.md](./threat-model.md#4-the-control-channel-internal-local-connection-in-detail).
Frontends never start `serve`: run it
explicitly in the foreground or let an external system service manager own its lifecycle.

The authority always lives on the extension side: the daemon approves no write on its own — it forwards the
request and blocks until a human decides in the browser. Details in [threat-model.md](./threat-model.md).
The daemon accepts two kinds of extension connection at once: exactly one active ScriptCat connection (a new
one replaces the previous one), and any number of paired `sctl Browser` instances connecting simultaneously,
each identified by its own instance ID. A call is routed by which peer kind owns its method (scripts.\* to
ScriptCat, tabs.\*/windows.\* to a resolved browser instance) — see
[protocol.md](./protocol.md#31-routing-and-target-selection). For ScriptCat's methods, the daemon approves no
write on its own — it forwards the request and blocks until a human decides in the browser, and source reads
go through the same disclosure gate. Browser control methods carry no such human gate by design: any
control-token holder can drive a paired browser instance immediately. Details in
[threat-model.md](./threat-model.md).

## Directory layout

Expand All @@ -37,6 +45,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)
browsers.go # sctl browsers [list] / sctl browsers forget <name|id>
tabs.go windows.go # sctl tabs list|open|close|activate, sctl windows list
resource.go # the optional scripts|script|sc resource word shared by those verbs
dispatch.go # action forwarding and bridge error → exit code mapping

Expand All @@ -50,7 +60,9 @@ internal/daemon/ # ── sctl serve side ──
envelope.go # envelope, payload structs, error codes
controlapi/ # /control/* handlers (controller role), depends on the narrow Bridge interface
auth/ # mutual HMAC handshake, enrollment-code derivation (HKDF), key delivery (AES-GCM)
store/ # persistence of the long-term key K (repository role)
store/ # persistence (repository role): ScriptCat's long-term key K, plus the
# browsers.json registry of paired sctl Browser instances and their own
# per-instance keys
ratelimit/ # enrollment-attempt rate limiting

internal/client/ # ── sctl mcp / CLI verb side ──
Expand All @@ -64,6 +76,13 @@ internal/pkg/ # ── shared by both sides ──
paths/ # data directory and derived paths
logging/ # unified zap logging (stderr + <dataDir>/logs/*.log, never stdout)
fsutil/ # atomic file writes

extension/ # ── sctl Browser, the second extension kind (MV3, pnpm/Vite/React) ──
src/background/ # service worker: identity and settings storage, message routing, method dispatch
src/offscreen/ # holds the WebSocket and connection state, pairing and session handshake, retry/backoff
src/handlers/ # tabs.*/windows.* method implementations (chrome.tabs / chrome.windows)
src/popup/ # popup UI: pairing, rename, forget, daemon address
src/protocol/generated/ # browser-only generated protocol TS (see protocol.md §6)
```

## Dependency direction
Expand Down
Loading
Loading