portable-agent-layer 0.45.2 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playwright
|
|
3
|
+
description: Capture a screenshot of a URL or local page and load it into context for a visual check. Use when asked to check visually, use playwright, screenshot a page, see or look at the design yourself, or verify a layout on desktop and mobile widths.
|
|
4
|
+
argument-hint: <url> [--viewport WxH] [--full-page] [--selector <css>]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Take a screenshot of a running page and `Read` it into context so you can see the rendered result yourself — for design review, responsive checks (desktop + mobile widths), and confirming a layout fix actually looks right. This skill returns an **image**, not page data.
|
|
10
|
+
|
|
11
|
+
## Engine selection (handled by the tool)
|
|
12
|
+
|
|
13
|
+
The bundled tool picks the best available local engine automatically:
|
|
14
|
+
|
|
15
|
+
1. **System `playwright-cli`** (Microsoft's stateful agent CLI) — used when it is on `PATH` and no exact viewport/full-page is requested (its `screenshot` command can't set those).
|
|
16
|
+
2. **PAL-installed Playwright, launched via Node** — the cross-platform-safe path (Playwright's Chromium hangs under Bun on Windows). Honors `--viewport`, `--full-page`, and `--selector` precisely.
|
|
17
|
+
|
|
18
|
+
If neither engine is usable, the tool prints `NO_PLAYWRIGHT_CLI` and exits non-zero — only then use the **Playwright MCP** (tier 3) in step 4.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
1. Resolve the URL. If it's a local app, make sure the dev server is already running first.
|
|
23
|
+
2. Run the tool (it prints the absolute PNG path as its last stdout line):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node --experimental-strip-types ~/.pal/skills/playwright/tools/shot.ts <url> \
|
|
27
|
+
[--viewport 1440x900] [--full-page] [--selector "<css>"] [-o <out.png>]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. `Read` the printed path so the screenshot enters your context. For a responsive check, run once per width (e.g. `--viewport 1440x900` then `--viewport 390x844`) and Read each.
|
|
31
|
+
4. If the tool exits with `NO_PLAYWRIGHT_CLI`:
|
|
32
|
+
- If Playwright MCP tools (`mcp__playwright__browser_*`) are available: `browser_navigate` to the URL, `browser_take_screenshot` to a file, then `Read` it.
|
|
33
|
+
- Otherwise, tell the user Playwright isn't installed and suggest running `pal cli install`.
|
|
34
|
+
|
|
35
|
+
## Output format
|
|
36
|
+
|
|
37
|
+
The screenshot image (via `Read`) plus one line stating the URL and viewport captured. Add a review only if the caller asked you to assess the design.
|
|
38
|
+
|
|
39
|
+
## When to use
|
|
40
|
+
|
|
41
|
+
- "check visually", "use playwright", "screenshot this", "see/look at the design yourself", "does this look right", desktop/mobile layout verification.
|
|
42
|
+
|
|
43
|
+
## Do NOT use
|
|
44
|
+
|
|
45
|
+
- To scrape page text or DOM data — fetch the page directly instead; this returns an image.
|
|
46
|
+
- To measure computed styles, element sizes, or horizontal overflow — use the Playwright MCP `browser_evaluate` for metrics; this skill does not return numbers.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure, side-effect-free helpers for the playwright skill tool.
|
|
2
|
+
// Kept separate from shot.ts so they can be unit-tested without launching a browser.
|
|
3
|
+
|
|
4
|
+
export type ShotOptions = {
|
|
5
|
+
url: string;
|
|
6
|
+
out: string;
|
|
7
|
+
viewport?: { width: number; height: number };
|
|
8
|
+
fullPage: boolean;
|
|
9
|
+
selector?: string;
|
|
10
|
+
waitMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const USAGE =
|
|
14
|
+
"usage: shot.ts <url> [-o <file>] [--viewport WxH] [--full-page] [--selector <css>] [--wait <ms>]";
|
|
15
|
+
|
|
16
|
+
export function parseArgs(argv: string[]): ShotOptions {
|
|
17
|
+
let url = "";
|
|
18
|
+
let out = "";
|
|
19
|
+
let viewport: ShotOptions["viewport"];
|
|
20
|
+
let fullPage = false;
|
|
21
|
+
let selector: string | undefined;
|
|
22
|
+
let waitMs: number | undefined;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "-o" || a === "--out") out = argv[++i] ?? "";
|
|
27
|
+
else if (a === "--viewport") {
|
|
28
|
+
const m = /^(\d+)[x,](\d+)$/.exec(argv[++i] ?? "");
|
|
29
|
+
if (!m) throw new Error("--viewport expects WxH, e.g. 1440x900");
|
|
30
|
+
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
31
|
+
} else if (a === "--full-page") fullPage = true;
|
|
32
|
+
else if (a === "--selector") selector = argv[++i];
|
|
33
|
+
else if (a === "--wait") {
|
|
34
|
+
const n = Number(argv[++i]);
|
|
35
|
+
if (!Number.isFinite(n)) throw new Error("--wait expects a number of milliseconds");
|
|
36
|
+
waitMs = n;
|
|
37
|
+
} else if (!a.startsWith("-") && !url) url = a;
|
|
38
|
+
else throw new Error(`unknown argument: ${a}\n${USAGE}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!url) throw new Error(`a URL is required\n${USAGE}`);
|
|
42
|
+
return { url, out, viewport, fullPage, selector, waitMs };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Tier = "cli" | "node";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decide which local engine to use. `playwright-cli` (Microsoft's stateful agent CLI)
|
|
49
|
+
* is preferred when present, but its `screenshot` command cannot set a viewport or
|
|
50
|
+
* capture full-page — so any request that needs exact size/full-page falls to the
|
|
51
|
+
* Node-launched PAL Playwright, which honors them precisely.
|
|
52
|
+
*/
|
|
53
|
+
export function chooseTier(opts: {
|
|
54
|
+
cliAvailable: boolean;
|
|
55
|
+
viewport?: unknown;
|
|
56
|
+
fullPage?: boolean;
|
|
57
|
+
}): Tier {
|
|
58
|
+
if (!opts.cliAvailable) return "node";
|
|
59
|
+
if (opts.viewport || opts.fullPage) return "node";
|
|
60
|
+
return "cli";
|
|
61
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// playwright skill tool: capture a screenshot of a URL and print its absolute path.
|
|
3
|
+
//
|
|
4
|
+
// Engine selection (see chooseTier):
|
|
5
|
+
// Tier 1 — system `playwright-cli` binary (Microsoft's stateful agent CLI), when on
|
|
6
|
+
// PATH and no exact viewport/full-page is requested.
|
|
7
|
+
// Tier 2 — PAL-installed Playwright, launched via Node. (Playwright's chromium.launch
|
|
8
|
+
// hangs under Bun on Windows — the same Node exception create-pdf relies on.)
|
|
9
|
+
// If neither engine works, prints NO_PLAYWRIGHT_CLI on stderr and exits non-zero so the
|
|
10
|
+
// caller (SKILL.md) can fall back to the Playwright MCP.
|
|
11
|
+
//
|
|
12
|
+
// Run with Node:
|
|
13
|
+
// node --experimental-strip-types ~/.pal/skills/playwright/tools/shot.ts <url> [opts]
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join, resolve } from "node:path";
|
|
19
|
+
import { chooseTier, parseArgs, type ShotOptions } from "./shot-lib.ts";
|
|
20
|
+
|
|
21
|
+
function playwrightCliAvailable(): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return spawnSync("playwright-cli", ["--version"], { stdio: "ignore" }).status === 0;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runViaCli(opts: ShotOptions, out: string): boolean {
|
|
30
|
+
const open = spawnSync("playwright-cli", ["open", opts.url], { stdio: "inherit" });
|
|
31
|
+
if (open.status !== 0) return false;
|
|
32
|
+
const args = ["screenshot", `--filename=${out}`];
|
|
33
|
+
if (opts.selector) args.push(opts.selector);
|
|
34
|
+
const shot = spawnSync("playwright-cli", args, { stdio: "inherit" });
|
|
35
|
+
spawnSync("playwright-cli", ["close"], { stdio: "ignore" });
|
|
36
|
+
return shot.status === 0 && existsSync(out);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// "unavailable" = the engine itself can't run (package or Chromium missing) → MCP fallback.
|
|
40
|
+
// "error" = the engine ran but the page/screenshot failed → a real error, not an MCP case.
|
|
41
|
+
type NodeResult = "ok" | "unavailable" | "error";
|
|
42
|
+
|
|
43
|
+
async function runViaNode(opts: ShotOptions, out: string): Promise<NodeResult> {
|
|
44
|
+
let chromium: typeof import("playwright").chromium;
|
|
45
|
+
try {
|
|
46
|
+
({ chromium } = await import("playwright"));
|
|
47
|
+
} catch {
|
|
48
|
+
return "unavailable"; // package not resolvable
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let browser: Awaited<ReturnType<typeof chromium.launch>>;
|
|
52
|
+
try {
|
|
53
|
+
browser = await chromium.launch();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`chromium launch failed: ${(e as Error).message}`);
|
|
56
|
+
return "unavailable"; // browser not installed / can't launch
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const page = await browser.newPage(opts.viewport ? { viewport: opts.viewport } : {});
|
|
61
|
+
await page.goto(opts.url, { waitUntil: "networkidle" });
|
|
62
|
+
if (opts.waitMs) await page.waitForTimeout(opts.waitMs);
|
|
63
|
+
if (opts.selector) await page.locator(opts.selector).screenshot({ path: out });
|
|
64
|
+
else await page.screenshot({ path: out, fullPage: opts.fullPage });
|
|
65
|
+
return existsSync(out) ? "ok" : "error";
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`screenshot failed: ${(e as Error).message}`);
|
|
68
|
+
return "error";
|
|
69
|
+
} finally {
|
|
70
|
+
await browser.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function main(): Promise<void> {
|
|
75
|
+
let opts: ShotOptions;
|
|
76
|
+
try {
|
|
77
|
+
opts = parseArgs(process.argv.slice(2));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error((e as Error).message);
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const out = opts.out ? resolve(opts.out) : join(tmpdir(), `pal-shot-${Date.now()}.png`);
|
|
84
|
+
const tier = chooseTier({
|
|
85
|
+
cliAvailable: playwrightCliAvailable(),
|
|
86
|
+
viewport: opts.viewport,
|
|
87
|
+
fullPage: opts.fullPage,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (tier === "cli" && runViaCli(opts, out)) {
|
|
91
|
+
console.log(out);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await runViaNode(opts, out);
|
|
96
|
+
if (result === "ok") {
|
|
97
|
+
console.log(out);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (result === "unavailable") {
|
|
101
|
+
console.error("NO_PLAYWRIGHT_CLI");
|
|
102
|
+
process.exit(3); // no local engine → caller falls back to Playwright MCP
|
|
103
|
+
}
|
|
104
|
+
process.exit(1); // engine ran but the capture failed — a real error, not an MCP case
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await main();
|