Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ jobs:
contents: read

build:
needs: test
# 二进制内嵌浏览器扩展(internal/pkg/browserext),要等扩展构建完成。
needs: [test, extension]
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -56,6 +57,10 @@ jobs:
with:
go-version-file: go.mod
cache: true
- uses: actions/download-artifact@v8
with:
name: extension-dist
path: internal/pkg/browserext/dist
- name: 构建并打包
env:
GOOS: ${{ matrix.goos }}
Expand Down Expand Up @@ -127,6 +132,12 @@ jobs:
name: release-extension
path: dist/*.zip
if-no-files-found: error
# 同一份产物供 build 任务内嵌进二进制;不带 release- 前缀,不会被当成发布产物上传。
- uses: actions/upload-artifact@v7
with:
name: extension-dist
path: extension/dist/
if-no-files-found: error

release:
name: Create Release
Expand Down Expand Up @@ -207,6 +218,19 @@ jobs:
-e "s|^ sha256 \".*\"$| sha256 \"${SHA256}\"|" \
Formula/sctl.rb
grep -qF "$URL" Formula/sctl.rb && grep -qF "$SHA256" Formula/sctl.rb
# 源码包没有 Node 构建产物:formula 以 resource 下载同版本的扩展 zip,放进内嵌目录再编译。
# 只改这个 resource 块里的行;tap 还没有这个块时照常更新源码包,只是装出的 sctl 不带扩展。
if grep -q '^ resource "browser-extension" do$' Formula/sctl.rb; then
EXT_URL="https://github.com/scriptscat/sctl/releases/download/${TAG}/sctl-browser-extension-${VERSION}.zip"
EXT_SHA256=$(curl -fsSL "$EXT_URL" | sha256sum | cut -d' ' -f1)
sed -i -E '/^ resource "browser-extension" do$/,/^ end$/ {
s|^ url ".*"$| url "'"${EXT_URL}"'"|
s|^ sha256 ".*"$| sha256 "'"${EXT_SHA256}"'"|
}' Formula/sctl.rb
grep -qF "$EXT_URL" Formula/sctl.rb && grep -qF "$EXT_SHA256" Formula/sctl.rb
else
echo "::warning::Formula/sctl.rb has no browser-extension resource; brew installs will not bundle the extension"
fi
# 重跑同一个 tag 时 formula 已是最新,不再产生空提交。
if git diff --quiet; then
echo "Formula/sctl.rb 已是 ${VERSION},跳过。"
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/sctl
/bin/
dist/
# 内嵌扩展的产物由 make extension-embed 拷入,只提交占位文件(见 internal/pkg/browserext)
!/internal/pkg/browserext/dist/
/internal/pkg/browserext/dist/*
!/internal/pkg/browserext/dist/.gitkeep
.dev-kit
# 一次性端到端验证脚本与证据(见 docs/verification.md),永远不进版本库
/e2e/scratch/
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ 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)
EMBED_DIR := internal/pkg/browserext/dist

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

help: ## 显示可用命令
@awk 'BEGIN {FS = ":.*## "; printf "用法: make <命令>\n\n命令:\n"} /^[a-zA-Z_-]+:.*## / {printf " %-10s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
Expand All @@ -25,6 +26,12 @@ test: ## 运行竞态检测测试
lint: ## 运行静态检查
$(GOLANGCI_LINT) run ./...

extension-embed: ## 构建浏览器扩展并拷进内嵌目录,之后的 build 带上扩展(需要 pnpm)
pnpm --dir extension install --frozen-lockfile
pnpm --dir extension build
find $(EMBED_DIR) -mindepth 1 ! -name .gitkeep -exec rm -rf {} +
cp -R extension/dist/. $(EMBED_DIR)/

dev: ## 构建并启动本地 daemon(DEV_VERSION=0.1.0)
mkdir -p $(BUILD_DIR)
$(GO) build -ldflags "-X $(VERSION_PACKAGE).Version=$(DEV_VERSION)" -o $(BINARY) ./cmd/sctl
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ The install script verifies the release archive's sha256 and installs `sctl` int
per extension.

- **ScriptCat:** enable **External Access** in its options and enter the code.
- **sctl Browser:** download `sctl-browser-extension-<version>.zip` from
[GitHub Releases](https://github.com/scriptscat/sctl/releases), unzip it, load the folder as an unpacked
extension, and enter a code in its popup. It needs **Chrome 125 or newer** (or a Chromium browser of that
version).
- **sctl Browser:** run `sctl browsers install`, load the directory it prints as an unpacked extension
(`chrome://extensions` → Developer mode → Load unpacked), and enter a code in its popup. sctl keeps that
directory up to date and reloads the extension after upgrades. It needs **Chrome 125 or newer** (or a
Chromium browser of that version).

3. **Check the connection:**

Expand Down Expand Up @@ -120,7 +120,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 get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` | Manage ScriptCat userscripts. |
| `sctl browsers` | List paired sctl Browser instances, or forget one. |
| `sctl browsers` | List paired sctl Browser instances, forget one, or install the bundled extension. |
| `sctl tabs` / `windows` / `groups` | Tabs, windows, and tab groups. |
| `sctl bookmarks` / `reading-list` | Bookmarks and the reading list. |
| `sctl history` / `recent` / `downloads` | History, recently closed tabs and windows, downloads. |
Expand Down
7 changes: 4 additions & 3 deletions docs/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ irm https://raw.githubusercontent.com/scriptscat/sctl/main/scripts/install.ps1 |
2. **配对扩展。** `sctl connect` 打印一次性配对码;一个码只能配对一个扩展,每个扩展各运行一次。

- **ScriptCat:** 在选项页启用 **External Access** 并输入配对码。
- **sctl Browser:** 从 [GitHub Releases](https://github.com/scriptscat/sctl/releases) 下载
`sctl-browser-extension-<version>.zip` 并解压,把解压后的目录作为“已解压的扩展程序”加载,在其弹窗里输入配对码。
- **sctl Browser:** 运行 `sctl browsers install`,把它打印的目录作为“已解压的扩展程序”加载
(`chrome://extensions` → 开发者模式 → 加载已解压的扩展程序),在其弹窗里输入配对码。sctl 会保持这个目录为最新,
升级后自动重新加载扩展。
要求 **Chrome 125 或更高版本**(或同版本的 Chromium 内核浏览器)。

3. **检查连接:**
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 get` / `grep` / `install` / `edit` / `enable` / `disable` / `delete` | 管理 ScriptCat 用户脚本。 |
| `sctl browsers` | 列出已配对的 sctl Browser 实例,或忘记其中一个。 |
| `sctl browsers` | 列出已配对的 sctl Browser 实例、忘记其中一个,或安装内嵌的扩展。 |
| `sctl tabs` / `windows` / `groups` | 标签页、窗口和标签组。 |
| `sctl bookmarks` / `reading-list` | 书签和阅读列表。 |
| `sctl history` / `recent` / `downloads` | 历史记录、最近关闭的标签页和窗口、下载。 |
Expand Down
6 changes: 5 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ 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>
browsers.go # sctl browsers [list] / sctl browsers forget <name|id> / sctl browsers install
tabs.go windows.go # sctl tabs list|open|close|activate, sctl windows list
page*.go # sctl page snapshot|click|hover|fill|type|press|select|upload|scroll|goto|back|forward|
# reload|wait|screenshot|eval|dialog|detach (reach /control/page through dispatchPage)
Expand All @@ -166,6 +166,8 @@ internal/daemon/ # ── sctl serve side ──
# page actions, per-tab debug record buffers, the bridge-backed CDP implementation
cdpendpoint/ # raw CDP endpoint: per-browser endpoints, /cdp/ HTTP and WS, Host/Origin/secret checks,
# expiry, hand-over to and from page, the per-client Hook and Browser
extupdate/ # keeps the sctl-managed extension directory on the bundled extension: on a handshake
# reporting an older bundle, rewrites the directory and calls extension.reload
auth/ # mutual HMAC handshake, enrollment-code derivation (HKDF), key delivery (AES-GCM)
store/ # persistence (repository role): ScriptCat's long-term key K, plus the
# browsers.json registry of paired sctl Browser instances and their own
Expand All @@ -183,6 +185,8 @@ 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
browserext/ # the sctl Browser extension embedded in the binary (dist/ is filled before go build)
# and writing it into the sctl-managed extension directory

extension/ # ── sctl Browser, the second extension kind (MV3, pnpm/Vite/React) ──
src/background/ # service worker: identity and settings storage, message routing, method dispatch
Expand Down
20 changes: 20 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ command line can use the skill in [`../skills/sctl/`](../skills/sctl/SKILL.md).
| Command | Purpose |
|---|---|
| `sctl browsers [list]` / `sctl browsers forget <name\|id>` | List paired sctl Browser instances, or forget one. |
| `sctl browsers install` | Write the sctl Browser extension bundled in the binary to a fixed directory to load in the browser; see [Installing sctl Browser](#installing-sctl-browser-sctl-browsers-install). |
| `sctl tabs list\|open\|close\|activate` | List, open, close, or activate tabs on a paired sctl Browser instance. |
| `sctl tabs move\|pin\|unpin\|mute\|unmute\|reload\|duplicate` | Move, pin, mute, reload, or duplicate tabs (several IDs are all-or-nothing). |
| `sctl windows list` | List windows on a paired sctl Browser instance. |
Expand Down Expand Up @@ -81,6 +82,25 @@ command line can use the skill in [`../skills/sctl/`](../skills/sctl/SKILL.md).
| `sctl cdp send <Method> [--params '<JSON object>'] [--tab N] [--timeout D]` | Send one raw Chrome DevTools Protocol command to a tab's page and print Chrome's result. |
| `sctl cdp endpoint` / `sctl cdp status` / `sctl cdp close` | Create or show a browser's CDP endpoint addresses for Playwright `connectOverCDP` and Puppeteer `connect`, show whether a client is connected and when the endpoint expires, or close it. |

## Installing sctl Browser: `sctl browsers install`

`sctl browsers install` writes the extension bundled in the binary to `<data dir>/browser-extension` and prints that
path, the extension version, and how to load it: open `chrome://extensions`, turn on Developer mode, choose
**Load unpacked**, and select the directory (the macOS dialog hides `~/Library`; press Cmd+Shift+G and paste the
path). Then pair it with a `sctl connect` code in its popup. It does not need the daemon, leaves the directory alone
when it already holds the same extension, and prints `{dir, version, changed}` with `-o json`. A binary built without
a bundled extension (a plain `go build`) exits 3; download the extension zip from the release instead.

The directory holds a `sctl-bundle.json` marker with the extension's content digest, which the extension reports when
it connects. When a daemon whose bundled extension differs sees that report — after you upgrade sctl and restart the
daemon — it rewrites the directory with its own bundle and has the extension reload itself from disk; the pairing
survives. Once the reload has reached an instance the daemon does not ask that instance again until it restarts (a
failure before that, such as the instance going offline, is retried at its next connection), and it never touches an
extension loaded from a release zip, which has no marker. The extension reports the digest of the code it is running,
read once after it loads, so a directory rewritten under a running extension (for example by `sctl browsers install`)
is caught the next time that extension connects. The directory always ends up with the daemon's bundle, even when the `sctl` that
ran `install` is a different version.

## Conventions

### Approval and immediate commands
Expand Down
11 changes: 10 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ It produces 7 artifacts — 6 sctl archives (darwin/linux/windows × amd64/arm64
`sctl-browser-extension-<version>.zip` — plus `checksums.txt`, generates build provenance for the checksum file,
and publishes the GitHub Release automatically after the full test gate passes. The extension zip holds one
`sctl-browser-extension-<version>/` directory for "Load unpacked"; its manifest version is the release version.
The `extension` job also hands its build output to the six binary builds, which copy it into
`internal/pkg/browserext/dist/` before `go build`, so every released binary carries the same extension for
`sctl browsers install`.

Artifacts are built with `-trimpath` and use the commit time as the archive member timestamp, so rebuilding
the same tag is byte-for-byte reproducible. Version, commit, and build time are injected into `internal/cli`
Expand All @@ -155,7 +158,9 @@ Cross-compilation, packaging, checksums, and the GitHub Release upload all happe

For a non-prerelease tag, the `homebrew` job then points `Formula/sctl.rb` in
[scriptscat/homebrew-tap](https://github.com/scriptscat/homebrew-tap) at the tag's source tarball and pushes the
commit. It authenticates with the `HOMEBREW_TAP_DEPLOY_KEY` secret, the private half of a write-enabled deploy key
commit. The formula builds from source without Node, so it also downloads the tag's extension zip as its
`browser-extension` resource and puts it into `internal/pkg/browserext/dist/`; the job updates that resource's URL and
SHA-256 too, and only warns when the formula has no such resource. It authenticates with the `HOMEBREW_TAP_DEPLOY_KEY` secret, the private half of a write-enabled deploy key
on the tap repository.

## Browser extension
Expand All @@ -173,6 +178,10 @@ pnpm test # Vitest, with chrome.* and WebSocket replaced by fakes
pnpm build # writes dist/, loadable through "Load unpacked"
```

A plain `go build` leaves `internal/pkg/browserext/dist/` holding only its placeholder, so the binary has no
bundled extension: `sctl browsers install` exits 3 and the daemon skips extension updates. `make extension-embed`
builds the extension and copies it there (it needs pnpm), after which `make build` or `go build` bundles it.

`SCTL_EXTENSION_VERSION` sets the manifest version for a build (the release workflow passes the tag version; a
prerelease suffix such as `-rc.1` goes into `version_name`, because Chrome versions are numeric only). Without it,
the build uses the `version` in `extension/package.json`. The generated protocol bindings in
Expand Down
19 changes: 11 additions & 8 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,17 @@ independently, but both use the same one-time-code flow against the same running
3. Enter the displayed one-time code:
- For ScriptCat: open its options page, enable **External Access**, and enter the code in the enrollment
dialog.
- For sctl Browser: download `sctl-browser-extension-<version>.zip` from
[GitHub Releases](https://github.com/scriptscat/sctl/releases) (its version matches the sctl release),
unzip it, open your browser's extensions page, enable developer mode, choose "Load unpacked", and select
the unzipped folder. Open the extension's popup and enter the same code.

sctl Browser requires Chrome 125 or newer and the `debugger` permission. When you update an unpacked copy,
reload it from the extensions page; Chrome will not load a build whose minimum version is above the running
browser's. While the extension has the debugger attached to a page, Chrome shows a "sctl Browser started
- For sctl Browser: run `sctl browsers install`, which writes the extension bundled in sctl to a fixed directory
and prints it. Open your browser's extensions page, enable developer mode, choose "Load unpacked", and select
that directory. Open the extension's popup and enter the same code. After you upgrade sctl and restart the
daemon, it updates that directory and reloads the extension for you
(see [`cli.md`](./cli.md#installing-sctl-browser-sctl-browsers-install)). Alternatively, download
`sctl-browser-extension-<version>.zip` from [GitHub Releases](https://github.com/scriptscat/sctl/releases)
(its version matches the sctl release) and load the unzipped folder the same way; such a copy is not updated
automatically, so reload it from the extensions page after replacing its files.

sctl Browser requires Chrome 125 or newer and the `debugger` permission; Chrome will not load a build whose
minimum version is above the running browser's. While the extension has the debugger attached to a page, Chrome shows a "sctl Browser started
debugging this browser" infobar at the top of the window; the extension cannot hide it, and dismissing it
detaches the debugger. Launch Chrome with `--silent-debugger-extension-api` to suppress the infobar.
4. Run `sctl connect` again for the second extension if you want to pair both — the code is valid only for a
Expand Down
Loading
Loading