Skip to content
Merged
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
57 changes: 57 additions & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion README-zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 下载)。
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
22 changes: 22 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 18 additions & 1 deletion extensions/browser/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,6 +70,22 @@ function readBrowserConfigSync(): BrowserConfig {
}
}

type PlaywrightCore = typeof import("playwright-core");
let playwrightCorePromise: Promise<PlaywrightCore> | undefined;

async function loadPlaywrightCore(): Promise<PlaywrightCore> {
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<Browser> {
const launchOpts: Record<string, unknown> = {
headless: opts.headless,
Expand All @@ -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);
}

Expand Down
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -56,21 +56,25 @@
"./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"
},
"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
}
}
}
4 changes: 2 additions & 2 deletions scripts/check-pack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
8 changes: 6 additions & 2 deletions scripts/smoke-built-index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -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);
};

Expand Down Expand Up @@ -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;
Expand Down
Loading