diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..a92b76e --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,57 @@ +name: Compatibility + +on: + workflow_dispatch: + schedule: + - cron: "29 3 * * 1" + +permissions: + contents: read + +jobs: + pi-compatibility: + name: ${{ matrix.channel }} / Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - channel: minimum + pi_version: "0.80.2" + node: 22 + - channel: minimum + pi_version: "0.80.2" + node: 24 + - channel: current + pi_version: "0.80.10" + node: 22 + - channel: current + pi_version: "0.80.10" + node: 24 + - channel: latest + pi_version: latest + node: 24 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - name: Install the matrix Pi host + env: + PI_VERSION: ${{ matrix.pi_version }} + run: | + npm install --ignore-scripts --no-save --package-lock=false --no-audit --no-fund \ + "@earendil-works/pi-coding-agent@${PI_VERSION}" \ + "@earendil-works/pi-tui@${PI_VERSION}" + - name: Report installed host versions + run: | + node - <<'NODE' + for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-tui"]) { + console.log(`${name}: ${require(`${name}/package.json`).version}`); + } + NODE + - run: npm run verify + - run: npm run pack:dry-run + - run: npm audit --omit=dev --audit-level=high diff --git a/README-zh-CN.md b/README-zh-CN.md index 602c9b0..06b514e 100644 --- a/README-zh-CN.md +++ b/README-zh-CN.md @@ -94,7 +94,16 @@ pi remove npm:@cynos-ai/tools ### 浏览器 -Tools 使用 `playwright-core`,**不**捆绑浏览器。首次使用时: +浏览器支持是可选的,因此普通的搜索/视觉安装不会自动拉取 Playwright +运行时。需要浏览器时,在宿主项目中显式安装可选 peer: + +```bash +npm install --save-dev playwright-core +``` + +没有安装它时,搜索、视觉和配置功能仍然可用;浏览器调用会返回明确的 +setup 错误,不会在 Tools 启动阶段直接失败。安装后,Tools 使用 +`playwright-core`,但**不**捆绑浏览器。首次使用时: 1. 检测到系统 Chrome / Chromium / Edge,则直接启动。 2. 否则 Tools 返回明确的 setup 指引。运行 `/cynos-tools-browser-setup` 探测,或通过 `playwright-core` 安装 Chromium(需要明确确认,约 150 MB 下载)。 diff --git a/README.md b/README.md index 96615f2..6db8c59 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,17 @@ Order: user-preferred REST → other configured REST → free Exa MCP. Search wo ### Browser -Tools uses `playwright-core` and does **not** bundle a browser. On first use: +Browser support is optional so ordinary search/vision installs do not pull in the +Playwright runtime. To enable browser tools in a host project, install the +optional peer explicitly: + +```bash +npm install --save-dev playwright-core +``` + +Without it, search, vision, and configuration still work; browser calls return a +clear setup error instead of failing during Tools startup. Once installed, Tools +uses `playwright-core` and does **not** bundle a browser. On first use: 1. If a system Chrome / Chromium / Edge is detected, Tools launches it directly. 2. Otherwise Tools returns a clear setup pointer. Run `/cynos-tools-browser-setup` to probe, or to install Chromium via `playwright-core` (explicit confirmation required — ~150 MB download). diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..ec2038f --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,22 @@ +# Compatibility matrix + +Tools supports Node.js `>=22` and the following Pi host ranges: + +| Host baseline | `@earendil-works/pi-coding-agent` | `@earendil-works/pi-tui` | Purpose | +| --- | --- | --- | --- | +| Minimum | `0.80.2` | `0.80.2` | Lowest version allowed by the published peer range | +| Current | `0.80.10` | `0.80.10` | Version pinned by the development lockfile | +| Latest | npm `latest` | npm `latest` | Early warning for the newest published host | + +The compatibility workflow runs the minimum and current baselines on Node 22 +and Node 24. It runs the floating `latest` host on Node 24. Every cell installs +the requested Pi host before running typecheck, unit tests, build smoke, package +validation, and the production dependency audit. + +`latest` is intentionally a scheduled/manual signal rather than a required +branch-protection check: a new upstream Pi release must not block an unrelated +Tools pull request. A failure still requires investigation before updating the +supported-current baseline. + +When the Pi peer range or development dependency changes, update this table and +`.github/workflows/compatibility.yml` in the same change. diff --git a/extensions/browser/launch.ts b/extensions/browser/launch.ts index 7d0433f..bfcd462 100644 --- a/extensions/browser/launch.ts +++ b/extensions/browser/launch.ts @@ -4,7 +4,7 @@ import { existsSync } from "node:fs"; import { spawn } from "node:child_process"; import * as path from "node:path"; -import { chromium, type Browser, type BrowserContext, type Page } from "playwright-core"; +import type { Browser, BrowserContext, Page } from "playwright-core"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { getBrowserConfig, type BrowserConfig } from "../config/store"; import { @@ -70,6 +70,22 @@ function readBrowserConfigSync(): BrowserConfig { } } +type PlaywrightCore = typeof import("playwright-core"); +let playwrightCorePromise: Promise | undefined; + +async function loadPlaywrightCore(): Promise { + if (!playwrightCorePromise) { + playwrightCorePromise = import("playwright-core").catch((error) => { + playwrightCorePromise = undefined; + const detail = error instanceof Error ? error.message : String(error); + throw new BrowserUnavailableError( + `Optional browser support is not installed. Add playwright-core to the host project before using browser tools.${detail ? ` (${detail})` : ""}`, + ); + }); + } + return playwrightCorePromise; +} + async function tryLaunch(opts: { executablePath?: string; channel?: "chrome" | "chromium" | "msedge"; headless: boolean; timeoutMs: number }): Promise { const launchOpts: Record = { headless: opts.headless, @@ -78,6 +94,7 @@ async function tryLaunch(opts: { executablePath?: string; channel?: "chrome" | " if (opts.executablePath) launchOpts.executablePath = opts.executablePath; else if (opts.channel) launchOpts.channel = opts.channel; + const { chromium } = await loadPlaywrightCore(); return chromium.launch(launchOpts); } diff --git a/package-lock.json b/package-lock.json index a156d09..0198000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,12 @@ "name": "@cynos-ai/tools", "version": "0.2.3", "license": "MIT", - "dependencies": { - "playwright-core": "1.61.1" - }, "devDependencies": { "@earendil-works/pi-coding-agent": "0.80.10", "@earendil-works/pi-tui": "0.80.10", "@types/node": "25.9.5", "esbuild": "0.28.1", + "playwright-core": "1.61.1", "typebox": "1.3.6", "typescript": "5.9.3", "vitest": "4.1.10" @@ -26,7 +24,13 @@ "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.80.2", "@earendil-works/pi-tui": ">=0.80.2", + "playwright-core": ">=1.61.1", "typebox": ">=1.0.59" + }, + "peerDependenciesMeta": { + "playwright-core": { + "optional": true + } } }, "node_modules/@earendil-works/pi-coding-agent": { @@ -3488,6 +3492,7 @@ "version": "1.61.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 1c5672f..395e74b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "vitest run", "verify": "npm run typecheck && npm test && npm run build:smoke", "build": "node scripts/build.mjs", - "build:smoke": "npm run build && node scripts/smoke-built-index.mjs", + "build:smoke": "npm run build && node scripts/smoke-built-index.mjs && node scripts/smoke-built-index.mjs --without-playwright", "pack:dry-run": "node scripts/check-pack.mjs", "prepack": "npm run build", "prepublishOnly": "npm run verify" @@ -56,14 +56,12 @@ "./index.js" ] }, - "dependencies": { - "playwright-core": "1.61.1" - }, "devDependencies": { "@earendil-works/pi-coding-agent": "0.80.10", "@earendil-works/pi-tui": "0.80.10", "@types/node": "25.9.5", "esbuild": "0.28.1", + "playwright-core": "1.61.1", "typebox": "1.3.6", "typescript": "5.9.3", "vitest": "4.1.10" @@ -71,6 +69,12 @@ "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.80.2", "@earendil-works/pi-tui": ">=0.80.2", - "typebox": ">=1.0.59" + "typebox": ">=1.0.59", + "playwright-core": ">=1.61.1" + }, + "peerDependenciesMeta": { + "playwright-core": { + "optional": true + } } } diff --git a/scripts/check-pack.mjs b/scripts/check-pack.mjs index d54bd3e..cc6c444 100644 --- a/scripts/check-pack.mjs +++ b/scripts/check-pack.mjs @@ -33,8 +33,8 @@ if (forbidden.length > 0) { process.exit(1); } -// playwright-core must be in node_modules (runtime dep), but its browser binaries -// must NOT be in the tarball. +// playwright-core is an optional peer; its browser binaries must NOT be in the +// tarball even when the development install has the peer available. const browserBinary = [...files].find((file) => /(^|\/)\.local-browsers\//.test(file) || /chromium-[0-9]/.test(file)); if (browserBinary) { console.error(`npm package includes a browser binary, which must not ship: ${browserBinary}`); diff --git a/scripts/smoke-built-index.mjs b/scripts/smoke-built-index.mjs index fb05934..765046b 100644 --- a/scripts/smoke-built-index.mjs +++ b/scripts/smoke-built-index.mjs @@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const indexPath = path.join(root, "index.js"); +const withoutPlaywright = process.argv.includes("--without-playwright"); if (!fs.existsSync(indexPath)) { console.error("Built artifact missing: index.js. Run npm run build first."); @@ -59,7 +60,10 @@ Module._load = function patchedLoad(request, parent, isMain) { if (request === "typebox") return typeboxStub; if (request === "typebox/compile") return { TypeCompiler: { Compile: () => ({ Check: () => true, Errors: () => [] }) } }; if (request === "typebox/value") return { Value: { Check: () => true, Errors: () => [], Parse: (_s, v) => v } }; - if (request === "playwright-core") return playwrightStub; + if (request === "playwright-core") { + if (withoutPlaywright) throw new Error("playwright-core should remain optional during core activation smoke"); + return playwrightStub; + } return originalLoad.call(this, request, parent, isMain); }; @@ -132,7 +136,7 @@ try { throw new Error(`new pi instance after reload should re-register all tools (got ${afterReload.registeredTools.length})`); } - console.log(`✓ built index.js smoke OK (main=${main.registeredTools.length} tools/${main.commands.length} cmds; researcher subset verified)`); + console.log(`✓ built index.js smoke OK (main=${main.registeredTools.length} tools/${main.commands.length} cmds; researcher subset verified${withoutPlaywright ? "; core activation works without playwright-core" : ""})`); } finally { Module._load = originalLoad; if (prevRole === undefined) delete process.env.CYNOS_AGENT_ROLE;