diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0327a1b..191419b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 并上传产物 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8542829..ebcd90b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 diff --git a/.gitignore b/.gitignore index c8ee25a..0dc96b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ .dev-kit # 一次性端到端验证脚本与证据(见 docs/verification.md),永远不进版本库 /e2e/scratch/ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 9923376..30af69f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/Makefile b/Makefile index 5435cfe..9ebf938 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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) diff --git a/README.md b/README.md index f1aabbc..a2fcbeb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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-.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 @@ -76,8 +86,8 @@ troubleshooting. | Command | Purpose | |---|---| | `sctl serve` | Run the local bridge daemon. | -| `sctl connect` | Open a one-time ScriptCat enrollment window. | -| `sctl mcp [--name